From eea2654236966fee69c4f18217a6a62b1c66d2da Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 13 Nov 2023 17:58:00 +0100
Subject: [PATCH 001/203] Fix format-dependent redirects being cached
regardless of requested format (#27634)
---
config/routes.rb | 23 +++++++++++++++++++----
spec/requests/cache_spec.rb | 16 +++++++++++-----
2 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/config/routes.rb b/config/routes.rb
index 5de8562a8c..7e2f1aabed 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,18 @@
require 'sidekiq_unique_jobs/web'
require 'sidekiq-scheduler/web'
+class RedirectWithVary < ActionDispatch::Routing::PathRedirect
+ def serve(...)
+ super.tap do |_, headers, _|
+ headers['Vary'] = 'Origin, Accept'
+ end
+ end
+end
+
+def redirect_with_vary(path)
+ RedirectWithVary.new(301, path)
+end
+
Rails.application.routes.draw do
# Paths of routes on the web app that to not require to be indexed or
# have alternative format representations requiring separate controllers
@@ -90,10 +102,13 @@ Rails.application.routes.draw do
confirmations: 'auth/confirmations',
}
- get '/users/:username', to: redirect('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
- get '/users/:username/following', to: redirect('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
- get '/users/:username/followers', to: redirect('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
- get '/users/:username/statuses/:id', to: redirect('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
+ # rubocop:disable Style/FormatStringToken - those do not go through the usual formatting functions and are not safe to correct
+ get '/users/:username', to: redirect_with_vary('/@%{username}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
+ get '/users/:username/following', to: redirect_with_vary('/@%{username}/following'), constraints: lambda { |req| req.format.nil? || req.format.html? }
+ get '/users/:username/followers', to: redirect_with_vary('/@%{username}/followers'), constraints: lambda { |req| req.format.nil? || req.format.html? }
+ get '/users/:username/statuses/:id', to: redirect_with_vary('/@%{username}/%{id}'), constraints: lambda { |req| req.format.nil? || req.format.html? }
+ # rubocop:enable Style/FormatStringToken
+
get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" }
resources :accounts, path: 'users', only: [:show], param: :username do
diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb
index c391c8b3da..d40895fc3b 100644
--- a/spec/requests/cache_spec.rb
+++ b/spec/requests/cache_spec.rb
@@ -124,7 +124,7 @@ describe 'Caching behavior' do
expect(response.cookies).to be_empty
end
- it 'sets public cache control' do
+ it 'sets public cache control', :aggregate_failures do
# expect(response.cache_control[:max_age]&.to_i).to be_positive
expect(response.cache_control[:public]).to be_truthy
expect(response.cache_control[:private]).to be_falsy
@@ -141,11 +141,8 @@ describe 'Caching behavior' do
end
shared_examples 'non-cacheable error' do
- it 'does not return HTTP success' do
+ it 'does not return HTTP success and does not have cache headers', :aggregate_failures do
expect(response).to_not have_http_status(200)
- end
-
- it 'does not have cache headers' do
expect(response.cache_control[:public]).to be_falsy
end
end
@@ -182,6 +179,15 @@ describe 'Caching behavior' do
end
context 'when anonymously accessed' do
+ describe '/users/alice' do
+ it 'redirects with proper cache header', :aggregate_failures do
+ get '/users/alice'
+
+ expect(response).to redirect_to('/@alice')
+ expect(response.headers['Vary']&.split(',')&.map { |x| x.strip.downcase }).to include('accept')
+ end
+ end
+
TestEndpoints::ALWAYS_CACHED.each do |endpoint|
describe endpoint do
before { get endpoint }
From 8383288219a5a0da41e581a727cfb70a742e3760 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Tue, 28 Nov 2023 12:54:03 +0900
Subject: [PATCH 002/203] =?UTF-8?q?Change:=20=E3=82=B5=E3=83=BC=E3=82=AF?=
=?UTF-8?q?=E3=83=AB=E3=81=AE=E9=80=81=E3=82=8A=E5=85=88=E3=82=A2=E3=82=AB?=
=?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E6=8C=87=E5=AE=9A=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=E3=82=92`account=5Fusername`=EF=BC=88Fedibird=E3=81=A8?=
=?UTF-8?q?=E5=90=8C=E6=A7=98=EF=BC=89=E3=81=AB=E5=A4=89=E6=9B=B4=20(#283)?=
=?UTF-8?q?=20(LTS)=20(#308)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Change: サークルの送り先アカウント指定方法を`account_username`(Fedibirdと同様)に変更 (#283)
* Change: サークルの送り先アカウント指定方法を`account_username`(Fedibirdと同様)に変更
* Test: テストを追加
* Maybe Fix: Fedibirdで自分限定と認識される問題
* Fix test
---
app/lib/activitypub/tag_manager.rb | 5 +---
app/lib/status_reach_finder.rb | 6 +++++
.../activitypub/distribution_worker.rb | 16 +++++++++++-
.../activitypub/distribution_worker_spec.rb | 26 ++++++++++++++++++-
4 files changed, 47 insertions(+), 6 deletions(-)
diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb
index f9b67867ef..52f7ed32ab 100644
--- a/app/lib/activitypub/tag_manager.rb
+++ b/app/lib/activitypub/tag_manager.rb
@@ -119,10 +119,7 @@ class ActivityPub::TagManager
end.compact
end
when 'limited'
- status.mentions.each_with_object([]) do |mention, result|
- result << uri_for(mention.account)
- result << followers_uri_for(mention.account) if mention.account.group?
- end.compact
+ ['kmyblue:Limited'] # to avoid Fedibird personal visibility
end
end
diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb
index 8639a30a9f..c7069c7dac 100644
--- a/app/lib/status_reach_finder.rb
+++ b/app/lib/status_reach_finder.rb
@@ -21,6 +21,12 @@ class StatusReachFinder
end
end
+ def inboxes_for_limited
+ DeliveryFailureTracker.without_unavailable(
+ @status.mentioned_accounts.where.not(domain: nil).pluck(:inbox_url).compact.uniq
+ )
+ end
+
private
def reached_account_inboxes
diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb
index 34b6f6e32f..57ee1fbc0a 100644
--- a/app/workers/activitypub/distribution_worker.rb
+++ b/app/workers/activitypub/distribution_worker.rb
@@ -7,13 +7,23 @@ class ActivityPub::DistributionWorker < ActivityPub::RawDistributionWorker
@status = Status.find(status_id)
@account = @status.account
- distribute!
+ if @status.limited_visibility?
+ distribute_limited!
+ else
+ distribute!
+ end
rescue ActiveRecord::RecordNotFound
true
end
protected
+ def distribute_limited!
+ ActivityPub::DeliveryWorker.push_bulk(inboxes_for_limited, limit: 1_000) do |inbox_url|
+ [payload, @account.id, inbox_url, options]
+ end
+ end
+
def inboxes
@inboxes ||= status_reach_finder.inboxes
end
@@ -22,6 +32,10 @@ class ActivityPub::DistributionWorker < ActivityPub::RawDistributionWorker
@inboxes_for_misskey ||= status_reach_finder.inboxes_for_misskey
end
+ def inboxes_for_limited
+ @inboxes_for_limited ||= status_reach_finder.inboxes_for_limited
+ end
+
def status_reach_finder
@status_reach_finder ||= StatusReachFinder.new(@status)
end
diff --git a/spec/workers/activitypub/distribution_worker_spec.rb b/spec/workers/activitypub/distribution_worker_spec.rb
index d8803f6b8a..aaf4dbfcce 100644
--- a/spec/workers/activitypub/distribution_worker_spec.rb
+++ b/spec/workers/activitypub/distribution_worker_spec.rb
@@ -6,7 +6,7 @@ describe ActivityPub::DistributionWorker do
subject { described_class.new }
let(:status) { Fabricate(:status) }
- let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') }
+ let(:follower) { Fabricate(:account, protocol: :activitypub, shared_inbox_url: 'http://example.com', inbox_url: 'http://example.com/follower/inbox', domain: 'example.com') }
describe '#perform' do
before do
@@ -24,6 +24,17 @@ describe ActivityPub::DistributionWorker do
end
end
+ context 'with unlisted status' do
+ before do
+ status.update(visibility: :unlisted)
+ end
+
+ it 'delivers to followers' do
+ expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com', anything]])
+ subject.perform(status.id)
+ end
+ end
+
context 'with private status' do
before do
status.update(visibility: :private)
@@ -35,6 +46,19 @@ describe ActivityPub::DistributionWorker do
end
end
+ context 'with limited status' do
+ before do
+ status.update(visibility: :limited)
+ status.capability_tokens.create!
+ status.mentions.create!(account: follower, silent: true)
+ end
+
+ it 'delivers to followers' do
+ expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[kind_of(String), status.account.id, 'http://example.com/follower/inbox', anything]])
+ subject.perform(status.id)
+ end
+ end
+
context 'with direct status' do
let(:mentioned_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://foo.bar/inbox', domain: 'foo.bar') }
From 129705db449e8bad727cba15bcf4938155842656 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Wed, 29 Nov 2023 13:29:15 +0900
Subject: [PATCH 003/203] =?UTF-8?q?Fix:=20=E3=82=B9=E3=82=BF=E3=83=B3?=
=?UTF-8?q?=E3=83=97=E3=82=92=E5=A4=96=E3=81=99=E3=81=A8=E3=81=8D=E3=80=81?=
=?UTF-8?q?=E8=87=AA=E5=88=86=E3=81=8C=E5=A4=96=E3=81=97=E3=81=9F=E3=82=B9?=
=?UTF-8?q?=E3=82=BF=E3=83=B3=E3=83=97=E3=81=8C=E6=8A=95=E7=A8=BF=E3=81=8B?=
=?UTF-8?q?=E3=82=89=E5=85=A8=E9=83=A8=E6=B6=88=E3=81=88=E3=82=8B=E5=95=8F?=
=?UTF-8?q?=E9=A1=8C=20(#317)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/models/status.rb | 2 ++
app/services/un_emoji_react_service.rb | 5 +++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/models/status.rb b/app/models/status.rb
index a5501fcbf6..69779eccb3 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -387,6 +387,8 @@ class Status < ApplicationRecord
end
public_emoji_reactions
+ else
+ emoji_reactions
end
end
end
diff --git a/app/services/un_emoji_react_service.rb b/app/services/un_emoji_react_service.rb
index b473d43459..90bc740f5c 100644
--- a/app/services/un_emoji_react_service.rb
+++ b/app/services/un_emoji_react_service.rb
@@ -39,8 +39,8 @@ class UnEmojiReactService < BaseService
end
def write_stream(emoji_reaction)
- emoji_group = @status.emoji_reactions_grouped_by_name
- .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.custom_emoji&.domain) }
+ emoji_group = @status.emoji_reactions_grouped_by_name(@account)
+ .find { |reaction_group| reaction_group['name'] == emoji_reaction.name }
if emoji_group
emoji_group['status_id'] = @status.id.to_s
else
@@ -48,6 +48,7 @@ class UnEmojiReactService < BaseService
emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @status.id.to_s }
emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end
+
DeliveryEmojiReactionWorker.perform_async(render_emoji_reaction(emoji_group), @status.id, emoji_reaction.account_id)
end
From ef149674f093e97ffbeb12910e2ebdd668bed7c2 Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 23 Oct 2023 14:27:07 +0200
Subject: [PATCH 004/203] Change Content-Security-Policy to be tighter on media
paths (#26889)
---
config/initializers/content_security_policy.rb | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 6ce84a6e42..85a328b448 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -5,7 +5,11 @@
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
def host_to_url(str)
- "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present?
+ return if str.blank?
+
+ uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}")
+ uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
+ uri.to_s
end
base_host = Rails.configuration.x.web_domain
From 156d32689b3d084a7fe9f0aa6a8788cd4e831f17 Mon Sep 17 00:00:00 2001
From: gunchleoc
Date: Thu, 28 Sep 2023 09:13:44 +0100
Subject: [PATCH 005/203] Only strip country code when language not listed in
SUPPORTED_LOCALES (#27099)
---
app/helpers/languages_helper.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb
index a8c66552cf..c42c4c23ef 100644
--- a/app/helpers/languages_helper.rb
+++ b/app/helpers/languages_helper.rb
@@ -254,6 +254,7 @@ module LanguagesHelper
def valid_locale_or_nil(str)
return if str.blank?
+ return str if valid_locale?(str)
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
From aa69ca74ed113e4f0526522931f24db55de90b9a Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 6 Oct 2023 17:46:04 +0200
Subject: [PATCH 006/203] Fix incorrect serialization of regional languages in
`contentMap` (#27207)
---
app/lib/activitypub/case_transform.rb | 2 ++
.../serializers/activitypub/note_serializer_spec.rb | 13 ++++++++++---
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb
index da2c5eb8b0..bf5de72210 100644
--- a/app/lib/activitypub/case_transform.rb
+++ b/app/lib/activitypub/case_transform.rb
@@ -14,6 +14,8 @@ module ActivityPub::CaseTransform
when String
camel_lower_cache[value] ||= if value.start_with?('_:')
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
+ elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
+ value
else
value.underscore.camelize(:lower)
end
diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb
index 4b2b8ec875..31ee31f132 100644
--- a/spec/serializers/activitypub/note_serializer_spec.rb
+++ b/spec/serializers/activitypub/note_serializer_spec.rb
@@ -7,7 +7,7 @@ describe ActivityPub::NoteSerializer do
let!(:account) { Fabricate(:account) }
let!(:other) { Fabricate(:account) }
- let!(:parent) { Fabricate(:status, account: account, visibility: :public) }
+ let!(:parent) { Fabricate(:status, account: account, visibility: :public, language: 'zh-TW') }
let!(:reply_by_account_first) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_account_next) { Fabricate(:status, account: account, thread: parent, visibility: :public) }
let!(:reply_by_other_first) { Fabricate(:status, account: other, thread: parent, visibility: :public) }
@@ -18,8 +18,15 @@ describe ActivityPub::NoteSerializer do
@serialization = ActiveModelSerializers::SerializableResource.new(parent, serializer: described_class, adapter: ActivityPub::Adapter)
end
- it 'has a Note type' do
- expect(subject['type']).to eql('Note')
+ it 'has the expected shape' do
+ expect(subject).to include({
+ '@context' => include('https://www.w3.org/ns/activitystreams'),
+ 'type' => 'Note',
+ 'attributedTo' => ActivityPub::TagManager.instance.uri_for(account),
+ 'contentMap' => include({
+ 'zh-TW' => a_kind_of(String),
+ }),
+ })
end
it 'has a replies collection' do
From cdedae6d63ff6ac21dd8810896d39f016fd8ec68 Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 23 Oct 2023 14:19:38 +0200
Subject: [PATCH 007/203] Fix some link anchors being recognized as hashtags
(#27271)
---
app/models/tag.rb | 2 +-
spec/models/tag_spec.rb | 26 +++++++++++++++-----------
2 files changed, 16 insertions(+), 12 deletions(-)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 672d80c8b8..413f6f5004 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -35,7 +35,7 @@ class Tag < ApplicationRecord
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
- HASHTAG_RE = %r{(?:^|[^/)\w])#(#{HASHTAG_NAME_PAT})}i
+ HASHTAG_RE = %r{(?
Date: Fri, 13 Oct 2023 18:15:47 +0900
Subject: [PATCH 008/203] Fix when unfollow a tag, my post also disappears from
the home timeline (#27391)
---
app/lib/feed_manager.rb | 1 +
spec/lib/feed_manager_spec.rb | 38 +++++++++++++++++++++++++++++++++++
2 files changed, 39 insertions(+)
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 26e5b78e8f..8b7f208115 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -192,6 +192,7 @@ class FeedManager
# also tagged with another followed hashtag or from a followed user
scope = from_tag.statuses
.where(id: timeline_status_ids)
+ .where.not(account: into_account)
.where.not(account: into_account.following)
.tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id))
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 25edaada64..f4dd42f845 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -525,6 +525,44 @@ RSpec.describe FeedManager do
end
end
+ describe '#unmerge_tag_from_home' do
+ let(:receiver) { Fabricate(:account) }
+ let(:tag) { Fabricate(:tag) }
+
+ it 'leaves a tagged status' do
+ status = Fabricate(:status)
+ status.tags << tag
+ described_class.instance.push_to_home(receiver, status)
+
+ described_class.instance.unmerge_tag_from_home(tag, receiver)
+
+ expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s)
+ end
+
+ it 'remains a tagged status written by receiver\'s followee' do
+ followee = Fabricate(:account)
+ receiver.follow!(followee)
+
+ status = Fabricate(:status, account: followee)
+ status.tags << tag
+ described_class.instance.push_to_home(receiver, status)
+
+ described_class.instance.unmerge_tag_from_home(tag, receiver)
+
+ expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
+ end
+
+ it 'remains a tagged status written by receiver' do
+ status = Fabricate(:status, account: receiver)
+ status.tags << tag
+ described_class.instance.push_to_home(receiver, status)
+
+ described_class.instance.unmerge_tag_from_home(tag, receiver)
+
+ expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s)
+ end
+ end
+
describe '#clear_from_home' do
let(:account) { Fabricate(:account) }
let(:followed_account) { Fabricate(:account) }
From 13205b54fd69f8cce1975aff8e784aa2f24e82ff Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 16 Oct 2023 15:24:14 +0200
Subject: [PATCH 009/203] Fix handling of `inLanguage` attribute in preview
card processing (#27423)
---
app/lib/link_details_extractor.rb | 3 ++-
spec/lib/link_details_extractor_spec.rb | 10 ++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb
index b95ec80519..a96612cab0 100644
--- a/app/lib/link_details_extractor.rb
+++ b/app/lib/link_details_extractor.rb
@@ -36,7 +36,8 @@ class LinkDetailsExtractor
end
def language
- json['inLanguage']
+ lang = json['inLanguage']
+ lang.is_a?(Hash) ? (lang['alternateName'] || lang['name']) : lang
end
def type
diff --git a/spec/lib/link_details_extractor_spec.rb b/spec/lib/link_details_extractor_spec.rb
index 599bc4e6de..8c485cef2a 100644
--- a/spec/lib/link_details_extractor_spec.rb
+++ b/spec/lib/link_details_extractor_spec.rb
@@ -82,6 +82,10 @@ RSpec.describe LinkDetailsExtractor do
'name' => 'Pet News',
'url' => 'https://example.com',
},
+ 'inLanguage' => {
+ name: 'English',
+ alternateName: 'en',
+ },
}.to_json
end
@@ -115,6 +119,12 @@ RSpec.describe LinkDetailsExtractor do
expect(subject.provider_name).to eq 'Pet News'
end
end
+
+ describe '#language' do
+ it 'returns the language from structured data' do
+ expect(subject.language).to eq 'en'
+ end
+ end
end
context 'when is wrapped in CDATA tags' do
From 700ae1f9183a01afe574687a91afb6e7a99825fc Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 26 Oct 2023 19:03:31 +0200
Subject: [PATCH 010/203] Fix report processing notice not mentioning the
report number when performing a custom action (#27442)
---
app/controllers/admin/account_actions_controller.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb
index e89404b609..e674bf55a0 100644
--- a/app/controllers/admin/account_actions_controller.rb
+++ b/app/controllers/admin/account_actions_controller.rb
@@ -21,7 +21,7 @@ module Admin
account_action.save!
if account_action.with_report?
- redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
+ redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else
redirect_to admin_account_path(@account.id)
end
From bece853e3cc45a7b8b8b19b78b6f772c65e80b2a Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 27 Oct 2023 10:35:21 +0200
Subject: [PATCH 011/203] Fix error and incorrect URLs in
`/api/v1/accounts/:id/featured_tags` for remote accounts (#27459)
---
.../rest/featured_tag_serializer.rb | 4 +-
config/routes.rb | 2 +-
.../accounts/featured_tags_controller_spec.rb | 23 ---------
.../api/v1/accounts/featured_tags_spec.rb | 50 +++++++++++++++++++
4 files changed, 54 insertions(+), 25 deletions(-)
delete mode 100644 spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
create mode 100644 spec/requests/api/v1/accounts/featured_tags_spec.rb
diff --git a/app/serializers/rest/featured_tag_serializer.rb b/app/serializers/rest/featured_tag_serializer.rb
index c4b35ab03a..c1ff4602aa 100644
--- a/app/serializers/rest/featured_tag_serializer.rb
+++ b/app/serializers/rest/featured_tag_serializer.rb
@@ -10,7 +10,9 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer
end
def url
- short_account_tag_url(object.account, object.tag)
+ # The path is hardcoded because we have to deal with both local and
+ # remote users, which are different routes
+ account_with_domain_url(object.account, "tagged/#{object.tag.to_param}")
end
def name
diff --git a/config/routes.rb b/config/routes.rb
index 7e2f1aabed..379262f4cd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -149,7 +149,7 @@ Rails.application.routes.draw do
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
end
- get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, format: false
+ get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, as: :account_with_domain, format: false
get '/settings', to: redirect('/settings/profile')
draw(:settings)
diff --git a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb b/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
deleted file mode 100644
index 53ac1e2a7a..0000000000
--- a/spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Api::V1::Accounts::FeaturedTagsController 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/featured_tags_spec.rb b/spec/requests/api/v1/accounts/featured_tags_spec.rb
new file mode 100644
index 0000000000..bae7d448b6
--- /dev/null
+++ b/spec/requests/api/v1/accounts/featured_tags_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'account featured tags 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/:id/featured_tags' do
+ subject do
+ get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers
+ end
+
+ before do
+ account.featured_tags.create!(name: 'foo')
+ account.featured_tags.create!(name: 'bar')
+ end
+
+ it 'returns the expected tags', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(body_as_json).to contain_exactly(a_hash_including({
+ name: 'bar',
+ url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar",
+ }), a_hash_including({
+ name: 'foo',
+ url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo",
+ }))
+ end
+
+ context 'when the account is remote' do
+ it 'returns the expected tags', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(body_as_json).to contain_exactly(a_hash_including({
+ name: 'bar',
+ url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar",
+ }), a_hash_including({
+ name: 'foo',
+ url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo",
+ }))
+ end
+ end
+ end
+end
From c66ade7de80e72d1dfdd1833c04cb3be47754f71 Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 20 Oct 2023 10:45:46 +0200
Subject: [PATCH 012/203] Fix processing LDSigned activities from actors with
unknown public keys (#27474)
---
app/lib/activitypub/linked_data_signature.rb | 6 ++--
.../activitypub/linked_data_signature_spec.rb | 34 +++++++++++++++++++
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb
index ea59879f3b..faea63e8f1 100644
--- a/app/lib/activitypub/linked_data_signature.rb
+++ b/app/lib/activitypub/linked_data_signature.rb
@@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017'
- creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
- creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
+ creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
+ creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil?
@@ -28,6 +28,8 @@ class ActivityPub::LinkedDataSignature
to_be_verified = options_hash + document_hash
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
+ rescue OpenSSL::PKey::RSAError
+ false
end
def sign!(creator, sign_with: nil)
diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb
index 6a6ad1a706..03d0c5a878 100644
--- a/spec/lib/activitypub/linked_data_signature_spec.rb
+++ b/spec/lib/activitypub/linked_data_signature_spec.rb
@@ -38,6 +38,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end
end
+ context 'when local account record is missing a public key' do
+ let(:raw_signature) do
+ {
+ 'creator' => 'http://example.com/alice',
+ 'created' => '2017-09-23T20:21:34Z',
+ }
+ end
+
+ let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
+
+ let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
+
+ before do
+ # Ensure signature is computed with the old key
+ signature
+
+ # Unset key
+ old_key = sender.public_key
+ sender.update!(private_key: '', public_key: '')
+
+ allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
+
+ allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
+ sender.update!(public_key: old_key)
+ sender
+ end
+ end
+
+ it 'fetches key and returns creator' do
+ expect(subject.verify_actor!).to eq sender
+ expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
+ end
+ end
+
context 'when signature is missing' do
let(:signature) { nil }
From d5bc10b711dede8c84087de9240eea85c14ab7b0 Mon Sep 17 00:00:00 2001
From: Renaud Chaput
Date: Thu, 19 Oct 2023 13:22:44 +0200
Subject: [PATCH 013/203] The `class` props should be `className` (#27462)
---
.../mastodon/features/ui/components/link_footer.jsx | 2 +-
.../mastodon/features/ui/components/navigation_panel.jsx | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/javascript/mastodon/features/ui/components/link_footer.jsx b/app/javascript/mastodon/features/ui/components/link_footer.jsx
index 9585df2ec4..6b1555243b 100644
--- a/app/javascript/mastodon/features/ui/components/link_footer.jsx
+++ b/app/javascript/mastodon/features/ui/components/link_footer.jsx
@@ -100,7 +100,7 @@ class LinkFooter extends PureComponent {
{DividingCircle}
{DividingCircle}
- v{version}
+ v{version}
);
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 8006ca89a2..22eee79c0a 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -59,10 +59,10 @@ class NavigationPanel extends Component {
{transientSingleColumn ? (
-
+
From 4fc252354624d5b0fbafd86251204cc19edc525d Mon Sep 17 00:00:00 2001
From: Renaud Chaput
Date: Thu, 19 Oct 2023 19:36:08 +0200
Subject: [PATCH 014/203] Do not display the navigation banner in the logo
container (#27476)
---
.../ui/components/navigation_panel.jsx | 30 +++++++++++--------
.../styles/mastodon/components.scss | 1 +
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 22eee79c0a..b16eb5e179 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -53,24 +53,30 @@ class NavigationPanel extends Component {
const { intl } = this.props;
const { signedIn, disabledAccountId } = this.context.identity;
+ let banner = undefined;
+
+ if(transientSingleColumn)
+ banner = ();
+
return (
-
- {transientSingleColumn ? (
-
- ) : (
-
- )}
+ {!banner &&
}
+ {banner &&
+
+ {banner}
+
+ }
+
{signedIn && (
<>
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 567397a07b..8c88b7dd6c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2466,6 +2466,7 @@ $ui-header-height: 55px;
.navigation-panel__sign-in-banner,
.navigation-panel__logo,
+ .navigation-panel__banner,
.getting-started__trends {
display: none;
}
From 4c3870647414f4cbd05373b0767d79e89a0db9a2 Mon Sep 17 00:00:00 2001
From: Claire
Date: Wed, 25 Oct 2023 15:55:57 +0200
Subject: [PATCH 015/203] Fix batch attachment deletion when using OpenStack
Swift (#27554)
---
app/lib/attachment_batch.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb
index b75938bdd7..78bd593160 100644
--- a/app/lib/attachment_batch.rb
+++ b/app/lib/attachment_batch.rb
@@ -75,7 +75,7 @@ class AttachmentBatch
end
when :fog
logger.debug { "Deleting #{attachment.path(style)}" }
- attachment.directory.files.new(key: attachment.path(style)).destroy
+ attachment.send(:directory).files.new(key: attachment.path(style)).destroy
when :azure
logger.debug { "Deleting #{attachment.path(style)}" }
attachment.destroy
From de86e822f4f51d7e20a59d48b2c7e66431e374fe Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 26 Oct 2023 15:09:48 +0200
Subject: [PATCH 016/203] Fix error when trying to delete already-deleted file
with OpenStack Swift (#27569)
---
app/lib/attachment_batch.rb | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb
index 78bd593160..13a9da828f 100644
--- a/app/lib/attachment_batch.rb
+++ b/app/lib/attachment_batch.rb
@@ -75,7 +75,12 @@ class AttachmentBatch
end
when :fog
logger.debug { "Deleting #{attachment.path(style)}" }
- attachment.send(:directory).files.new(key: attachment.path(style)).destroy
+
+ begin
+ attachment.send(:directory).files.new(key: attachment.path(style)).destroy
+ rescue Fog::Storage::OpenStack::NotFound
+ # Ignore failure to delete a file that has already been deleted
+ end
when :azure
logger.debug { "Deleting #{attachment.path(style)}" }
attachment.destroy
From e6f4c91c5c7a28b5185f31c0dc5b58c242f4270f Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 27 Oct 2023 16:04:51 +0200
Subject: [PATCH 017/203] Fix hashtag matching pattern matching some URLs
(#27584)
---
app/models/tag.rb | 2 +-
spec/models/tag_spec.rb | 4 ++++
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 413f6f5004..8fab98fb5c 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -35,7 +35,7 @@ class Tag < ApplicationRecord
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
- HASHTAG_RE = %r{(?
Date: Mon, 30 Oct 2023 23:32:25 +0100
Subject: [PATCH 018/203] Fix posts from force-sensitized accounts being able
to trend (#27620)
---
app/models/trends/statuses.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/trends/statuses.rb b/app/models/trends/statuses.rb
index 427fa8e797..c47fb8427b 100644
--- a/app/models/trends/statuses.rb
+++ b/app/models/trends/statuses.rb
@@ -106,7 +106,7 @@ class Trends::Statuses < Trends::Base
private
def eligible?(status)
- status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
+ status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
end
def calculate_scores(statuses, at_time)
From 54a07731d14ed3809375b31e2ce86e347afed271 Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 2 Nov 2023 15:58:37 +0100
Subject: [PATCH 019/203] Fix posts from threads received out-of-order
sometimes not being inserted into timelines (#27653)
---
app/services/fan_out_on_write_service.rb | 8 +-
app/workers/thread_resolve_worker.rb | 9 +-
spec/lib/activitypub/activity/create_spec.rb | 103 +++++++++++++++++++
3 files changed, 116 insertions(+), 4 deletions(-)
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 2554756a5d..f2a79c9fc9 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -8,6 +8,7 @@ class FanOutOnWriteService < BaseService
# @param [Hash] options
# @option options [Boolean] update
# @option options [Array] silenced_account_ids
+ # @option options [Boolean] skip_notifications
def call(status, options = {})
@status = status
@account = status.account
@@ -37,8 +38,11 @@ class FanOutOnWriteService < BaseService
def fan_out_to_local_recipients!
deliver_to_self!
- notify_mentioned_accounts!
- notify_about_update! if update?
+
+ unless @options[:skip_notifications]
+ notify_mentioned_accounts!
+ notify_about_update! if update?
+ end
case @status.visibility.to_sym
when :public, :unlisted, :private
diff --git a/app/workers/thread_resolve_worker.rb b/app/workers/thread_resolve_worker.rb
index 3206c45f63..d4cefb3fdc 100644
--- a/app/workers/thread_resolve_worker.rb
+++ b/app/workers/thread_resolve_worker.rb
@@ -7,13 +7,18 @@ class ThreadResolveWorker
sidekiq_options queue: 'pull', retry: 3
def perform(child_status_id, parent_url, options = {})
- child_status = Status.find(child_status_id)
- parent_status = FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
+ child_status = Status.find(child_status_id)
+ return if child_status.in_reply_to_id.present?
+
+ parent_status = ActivityPub::TagManager.instance.uri_to_resource(parent_url, Status)
+ parent_status ||= FetchRemoteStatusService.new.call(parent_url, **options.deep_symbolize_keys)
return if parent_status.nil?
child_status.thread = parent_status
child_status.save!
+
+ DistributionWorker.perform_async(child_status_id, { 'skip_notifications' => true }) if child_status.within_realtime_window?
rescue ActiveRecord::RecordNotFound
true
end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index f6c24754c0..8425f2127c 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -23,6 +23,109 @@ RSpec.describe ActivityPub::Activity::Create do
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
end
+ describe 'processing posts received out of order' do
+ let(:follower) { Fabricate(:account, username: 'bob') }
+
+ let(:object_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), 'post1'].join('/'),
+ type: 'Note',
+ to: [
+ 'https://www.w3.org/ns/activitystreams#Public',
+ ActivityPub::TagManager.instance.uri_for(follower),
+ ],
+ content: '@bob lorem ipsum',
+ published: 1.hour.ago.utc.iso8601,
+ updated: 1.hour.ago.utc.iso8601,
+ tag: {
+ type: 'Mention',
+ href: ActivityPub::TagManager.instance.uri_for(follower),
+ },
+ }
+ end
+
+ let(:reply_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), 'reply'].join('/'),
+ type: 'Note',
+ inReplyTo: object_json[:id],
+ to: [
+ 'https://www.w3.org/ns/activitystreams#Public',
+ ActivityPub::TagManager.instance.uri_for(follower),
+ ],
+ content: '@bob lorem ipsum',
+ published: Time.now.utc.iso8601,
+ updated: Time.now.utc.iso8601,
+ tag: {
+ type: 'Mention',
+ href: ActivityPub::TagManager.instance.uri_for(follower),
+ },
+ }
+ end
+
+ def activity_for_object(json)
+ {
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ id: [json[:id], 'activity'].join('/'),
+ type: 'Create',
+ actor: ActivityPub::TagManager.instance.uri_for(sender),
+ object: json,
+ }.with_indifferent_access
+ end
+
+ before do
+ follower.follow!(sender)
+ end
+
+ around do |example|
+ Sidekiq::Testing.fake! do
+ example.run
+ Sidekiq::Worker.clear_all
+ end
+ end
+
+ it 'correctly processes posts and inserts them in timelines', :aggregate_failures do
+ # Simulate a temporary failure preventing from fetching the parent post
+ stub_request(:get, object_json[:id]).to_return(status: 500)
+
+ # When receiving the reply…
+ described_class.new(activity_for_object(reply_json), sender, delivery: true).perform
+
+ # NOTE: Refering explicitly to the workers is a bit awkward
+ DistributionWorker.drain
+ FeedInsertWorker.drain
+
+ # …it creates a status with an unknown parent
+ reply = Status.find_by(uri: reply_json[:id])
+ expect(reply.reply?).to be true
+ expect(reply.in_reply_to_id).to be_nil
+
+ # …and creates a notification
+ expect(LocalNotificationWorker.jobs.size).to eq 1
+
+ # …but does not insert it into timelines
+ expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_nil
+
+ # When receiving the parent…
+ described_class.new(activity_for_object(object_json), sender, delivery: true).perform
+
+ Sidekiq::Worker.drain_all
+
+ # …it creates a status and insert it into timelines
+ parent = Status.find_by(uri: object_json[:id])
+ expect(parent.reply?).to be false
+ expect(parent.in_reply_to_id).to be_nil
+ expect(reply.reload.in_reply_to_id).to eq parent.id
+
+ # Check that the both statuses have been inserted into the home feed
+ expect(redis.zscore(FeedManager.instance.key(:home, follower.id), parent.id)).to be_within(0.1).of(parent.id.to_f)
+ expect(redis.zscore(FeedManager.instance.key(:home, follower.id), reply.id)).to be_within(0.1).of(reply.id.to_f)
+
+ # Creates two notifications
+ expect(Notification.count).to eq 2
+ end
+ end
+
describe '#perform' do
context 'when fetching' do
subject { described_class.new(json, sender) }
From 1076a6cd62a80f1e331980e7c81174fa1a67f435 Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 6 Nov 2023 10:28:14 +0100
Subject: [PATCH 020/203] Fix incoming status creation date not being
restricted to standard ISO8601 (#27655)
---
app/lib/activitypub/parser/status_parser.rb | 3 +-
spec/lib/activitypub/activity/create_spec.rb | 48 ++++++++++++++++++--
2 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb
index 3ba154d015..45f5fc5bf2 100644
--- a/app/lib/activitypub/parser/status_parser.rb
+++ b/app/lib/activitypub/parser/status_parser.rb
@@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
end
def created_at
- @object['published']&.to_datetime
+ datetime = @object['published']&.to_datetime
+ datetime if datetime.present? && (0..9999).cover?(datetime.year)
rescue ArgumentError
nil
end
diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb
index 8425f2127c..7594efd591 100644
--- a/spec/lib/activitypub/activity/create_spec.rb
+++ b/spec/lib/activitypub/activity/create_spec.rb
@@ -134,6 +134,46 @@ RSpec.describe ActivityPub::Activity::Create do
subject.perform
end
+ context 'when object publication date is below ISO8601 range' do
+ let(:object_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+ type: 'Note',
+ content: 'Lorem ipsum',
+ published: '-0977-11-03T08:31:22Z',
+ }
+ end
+
+ it 'creates status with a valid creation date', :aggregate_failures do
+ status = sender.statuses.first
+
+ expect(status).to_not be_nil
+ expect(status.text).to eq 'Lorem ipsum'
+
+ expect(status.created_at).to be_within(30).of(Time.now.utc)
+ end
+ end
+
+ context 'when object publication date is above ISO8601 range' do
+ let(:object_json) do
+ {
+ id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
+ type: 'Note',
+ content: 'Lorem ipsum',
+ published: '10000-11-03T08:31:22Z',
+ }
+ end
+
+ it 'creates status with a valid creation date', :aggregate_failures do
+ status = sender.statuses.first
+
+ expect(status).to_not be_nil
+ expect(status.text).to eq 'Lorem ipsum'
+
+ expect(status.created_at).to be_within(30).of(Time.now.utc)
+ end
+ end
+
context 'when object has been edited' do
let(:object_json) do
{
@@ -145,18 +185,16 @@ RSpec.describe ActivityPub::Activity::Create do
}
end
- it 'creates status' do
+ it 'creates status with appropriate creation and edition dates', :aggregate_failures do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
- end
- it 'marks status as edited' do
- status = sender.statuses.first
+ expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime
- expect(status).to_not be_nil
expect(status.edited?).to be true
+ expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime
end
end
From 8d02e58ff42d28f62a354c1ce8282924d54c455f Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 16 Nov 2023 14:43:02 +0100
Subject: [PATCH 021/203] Fix upper border radius of onboarding columns
(#27890)
---
app/javascript/styles/mastodon/components.scss | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 8c88b7dd6c..fbad5e2dbf 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2239,8 +2239,7 @@ $ui-header-height: 55px;
> .scrollable {
background: $ui-base-color;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
+ border-radius: 0 0 4px 4px;
}
}
From 252ea2fc67a8e95532c1887fc9f2842ddae189f3 Mon Sep 17 00:00:00 2001
From: Jonathan de Jong
Date: Fri, 27 Oct 2023 16:55:00 +0200
Subject: [PATCH 022/203] Have `Follow` activities bypass availability (#27586)
Co-authored-by: Claire
---
app/services/follow_service.rb | 2 +-
app/workers/activitypub/delivery_worker.rb | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index feea40e3c0..1aa0241fe6 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -71,7 +71,7 @@ class FollowService < BaseService
if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
elsif @target_account.activitypub?
- ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
+ ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
end
follow_request
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 7c1c14766b..376c237a98 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -23,9 +23,10 @@ class ActivityPub::DeliveryWorker
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {})
- return unless DeliveryFailureTracker.available?(inbox_url)
-
@options = options.with_indifferent_access
+
+ return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url)
+
@json = json
@source_account = Account.find(source_account_id)
@inbox_url = inbox_url
From e11100d7824258cafbd08b56945f340720a8029a Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 27 Nov 2023 14:25:54 +0100
Subject: [PATCH 023/203] Clamp dates when serializing to Elasticsearch API
(#28081)
---
app/chewy/accounts_index.rb | 4 +++-
app/chewy/concerns/datetime_clamping_concern.rb | 14 ++++++++++++++
app/chewy/public_statuses_index.rb | 4 +++-
app/chewy/statuses_index.rb | 4 +++-
app/chewy/tags_index.rb | 4 +++-
5 files changed, 26 insertions(+), 4 deletions(-)
create mode 100644 app/chewy/concerns/datetime_clamping_concern.rb
diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb
index 00db257ac7..59f2f991f2 100644
--- a/app/chewy/accounts_index.rb
+++ b/app/chewy/accounts_index.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
+ include DatetimeClampingConcern
+
settings index: index_preset(refresh_interval: '30s'), analysis: {
filter: {
english_stop: {
@@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index
field(:following_count, type: 'long')
field(:followers_count, type: 'long')
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
- field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
+ field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
diff --git a/app/chewy/concerns/datetime_clamping_concern.rb b/app/chewy/concerns/datetime_clamping_concern.rb
new file mode 100644
index 0000000000..7f176b6e54
--- /dev/null
+++ b/app/chewy/concerns/datetime_clamping_concern.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module DatetimeClampingConcern
+ extend ActiveSupport::Concern
+
+ MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
+ MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
+
+ class_methods do
+ def clamp_date(datetime)
+ datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
+ end
+ end
+end
diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb
index 4be204d4a9..076f72e525 100644
--- a/app/chewy/public_statuses_index.rb
+++ b/app/chewy/public_statuses_index.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class PublicStatusesIndex < Chewy::Index
+ include DatetimeClampingConcern
+
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
filter: {
english_stop: {
@@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
field(:language, type: 'keyword')
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
- field(:created_at, type: 'date')
+ field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
end
end
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 6b25dc9dff..c717de66ff 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class StatusesIndex < Chewy::Index
+ include DatetimeClampingConcern
+
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
filter: {
english_stop: {
@@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
field(:language, type: 'keyword')
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
- field(:created_at, type: 'date')
+ field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
end
end
diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb
index 5b6349a964..c99218a47f 100644
--- a/app/chewy/tags_index.rb
+++ b/app/chewy/tags_index.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
+ include DatetimeClampingConcern
+
settings index: index_preset(refresh_interval: '30s'), analysis: {
analyzer: {
content: {
@@ -42,6 +44,6 @@ class TagsIndex < Chewy::Index
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
- field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at })
+ field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) })
end
end
From 09115731d617c556135ac465421645ed54e92ca9 Mon Sep 17 00:00:00 2001
From: Claire
Date: Fri, 24 Nov 2023 10:31:28 +0100
Subject: [PATCH 024/203] Change GIF max matrix size error to explicitly
mention GIF files (#27927)
---
app/models/concerns/attachmentable.rb | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb
index c0ee1bdce7..4cdbdeb473 100644
--- a/app/models/concerns/attachmentable.rb
+++ b/app/models/concerns/attachmentable.rb
@@ -52,9 +52,13 @@ module Attachmentable
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
width, height = FastImage.size(attachment.queued_for_write[:original].path)
- matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
+ return unless width.present? && height.present?
- raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
+ if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
+ raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
+ elsif width * height > MAX_MATRIX_LIMIT
+ raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
+ end
end
def appropriate_extension(attachment)
From 7b9496322fd726a70c6fec1eee77c8cd04f0fd9e Mon Sep 17 00:00:00 2001
From: Claire
Date: Thu, 30 Nov 2023 12:45:54 +0100
Subject: [PATCH 025/203] Change dismissed banners to be stored server-side
(#27055)
---
.../components/dismissable_banner.tsx | 25 ++++++++++++++++---
app/javascript/mastodon/reducers/settings.js | 9 +++++++
2 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx
index 04a28e3cbe..6984691409 100644
--- a/app/javascript/mastodon/components/dismissable_banner.tsx
+++ b/app/javascript/mastodon/components/dismissable_banner.tsx
@@ -1,9 +1,16 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call,
+ @typescript-eslint/no-unsafe-return,
+ @typescript-eslint/no-unsafe-assignment,
+ @typescript-eslint/no-unsafe-member-access
+ -- the settings store is not yet typed */
import type { PropsWithChildren } from 'react';
-import { useCallback, useState } from 'react';
+import { useCallback, useState, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
+import { changeSetting } from 'mastodon/actions/settings';
import { bannerSettings } from 'mastodon/settings';
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { IconButton } from './icon_button';
@@ -19,13 +26,25 @@ export const DismissableBanner: React.FC> = ({
id,
children,
}) => {
- const [visible, setVisible] = useState(!bannerSettings.get(id));
+ const dismissed = useAppSelector((state) =>
+ state.settings.getIn(['dismissed_banners', id], false),
+ );
+ const dispatch = useAppDispatch();
+
+ const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
const intl = useIntl();
const handleDismiss = useCallback(() => {
setVisible(false);
bannerSettings.set(id, true);
- }, [id]);
+ dispatch(changeSetting(['dismissed_banners', id], true));
+ }, [id, dispatch]);
+
+ useEffect(() => {
+ if (!visible && !dismissed) {
+ dispatch(changeSetting(['dismissed_banners', id], true));
+ }
+ }, [id, dispatch, visible, dismissed]);
if (!visible) {
return null;
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 07d1bda0f4..a605ecbb8b 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -100,6 +100,15 @@ const initialState = ImmutableMap({
body: '',
}),
}),
+
+ dismissed_banners: ImmutableMap({
+ 'public_timeline': false,
+ 'community_timeline': false,
+ 'home.explore_prompt': false,
+ 'explore/links': false,
+ 'explore/statuses': false,
+ 'explore/tags': false,
+ }),
});
const defaultColumns = fromJS([
From 4b8fe9df73cf27db68f3c92178e37eff9deb3bd3 Mon Sep 17 00:00:00 2001
From: Claire
Date: Mon, 27 Nov 2023 15:00:52 +0100
Subject: [PATCH 026/203] Bump version to v4.2.2
---
CHANGELOG.md | 27 +++++++++++++++++++++++++++
SECURITY.md | 2 +-
docker-compose.yml | 6 +++---
lib/mastodon/version.rb | 2 +-
4 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f9303f0115..54bc2b4038 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
+## [4.2.2] - 2023-12-04
+
+### Changed
+
+- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
+- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
+- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
+- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
+- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
+- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
+
+### Fixed
+
+- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
+- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
+- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
+- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
+- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
+- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
+- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
+- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
+- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
+- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
+- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
+- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
+- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
+
## [4.2.1] - 2023-10-10
### Added
diff --git a/SECURITY.md b/SECURITY.md
index 3e13377db6..954ff73a24 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -17,6 +17,6 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| ------- | ---------------- |
| 4.2.x | Yes |
| 4.1.x | Yes |
-| 4.0.x | Until 2023-10-31 |
+| 4.0.x | No |
| 3.5.x | Until 2023-12-31 |
| < 3.5 | No |
diff --git a/docker-compose.yml b/docker-compose.yml
index 1a180b0890..efb8e528eb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -56,7 +56,7 @@ services:
web:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.1
+ image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@@ -77,7 +77,7 @@ services:
streaming:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.1
+ image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: node ./streaming
@@ -95,7 +95,7 @@ services:
sidekiq:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.1
+ image: ghcr.io/mastodon/mastodon:v4.2.2
restart: always
env_file: .env.production
command: bundle exec sidekiq
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 00538d07cc..7d5578d823 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
end
def patch
- 1
+ 2
end
def default_prerelease
From 2af8b653cb3aefb3addcedc88381dd5c7a1260f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Tue, 5 Dec 2023 09:33:54 +0900
Subject: [PATCH 027/203] Release: 5.10 LTS (#324)
---
lib/mastodon/version.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index cc60591f8a..5e400fa6f4 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,7 +9,7 @@ module Mastodon
end
def kmyblue_minor
- 9
+ 10
end
def kmyblue_flag
From 71b60b09f494ddd4670128cf252b6372a293a51e Mon Sep 17 00:00:00 2001
From: Claire
Date: Tue, 5 Dec 2023 14:15:33 +0100
Subject: [PATCH 028/203] Update dependency json-ld to v3.3.1
---
Gemfile.lock | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index a003cd18dd..76925a2bf2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -148,6 +148,7 @@ GEM
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
+ bcp47_spec (0.2.1)
bcrypt (3.1.18)
better_errors (2.10.1)
erubi (>= 1.0.0)
@@ -377,19 +378,19 @@ GEM
ipaddress (0.8.3)
jmespath (1.6.2)
json (2.6.3)
- 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.2.5)
+ 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)
- rdf (~> 3.2, >= 3.2.10)
+ rdf (~> 3.3)
json-ld-preloaded (3.2.2)
json-ld (~> 3.2)
rdf (~> 3.2)
@@ -593,7 +594,8 @@ GEM
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
- rdf (3.2.11)
+ rdf (3.3.1)
+ bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.1)
rdf (~> 3.2)
From 90371a4fc4da90c692e632b3231d804e310de916 Mon Sep 17 00:00:00 2001
From: Claire
Date: Tue, 5 Dec 2023 14:21:18 +0100
Subject: [PATCH 029/203] Bump version to v4.2.3
---
CHANGELOG.md | 6 ++++++
docker-compose.yml | 6 +++---
lib/mastodon/version.rb | 2 +-
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54bc2b4038..5f970519e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
+## [4.2.3] - 2023-12-05
+
+### Fixed
+
+- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
+
## [4.2.2] - 2023-12-04
### Changed
diff --git a/docker-compose.yml b/docker-compose.yml
index efb8e528eb..dda71cb091 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -56,7 +56,7 @@ services:
web:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.2
+ image: ghcr.io/mastodon/mastodon:v4.2.3
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@@ -77,7 +77,7 @@ services:
streaming:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.2
+ image: ghcr.io/mastodon/mastodon:v4.2.3
restart: always
env_file: .env.production
command: node ./streaming
@@ -95,7 +95,7 @@ services:
sidekiq:
build: .
- image: ghcr.io/mastodon/mastodon:v4.2.2
+ image: ghcr.io/mastodon/mastodon:v4.2.3
restart: always
env_file: .env.production
command: bundle exec sidekiq
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 7d5578d823..2e68df4744 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -13,7 +13,7 @@ module Mastodon
end
def patch
- 2
+ 3
end
def default_prerelease
From d79c88394b10cf10f45e8b0bc15282137f525301 Mon Sep 17 00:00:00 2001
From: KMY
Date: Wed, 6 Dec 2023 08:16:24 +0900
Subject: [PATCH 030/203] Bump version to 5.11
---
lib/mastodon/version.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index ef91183f82..e23a4afa94 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,7 +9,7 @@ module Mastodon
end
def kmyblue_minor
- 10
+ 11
end
def kmyblue_flag
From 5cce29495353813d86872578886a78f6f0c83d8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Tue, 12 Dec 2023 09:52:08 +0900
Subject: [PATCH 031/203] =?UTF-8?q?Fix:=20#284=20`FetchInstanceInfoWorker`?=
=?UTF-8?q?=E3=81=8C=E5=8E=9F=E5=9B=A0=E3=81=A7Sidekiq=E3=81=AEJob?=
=?UTF-8?q?=E3=81=8C=E8=A9=B0=E3=81=BE=E3=82=8B=E5=95=8F=E9=A1=8C=20(#342)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix: #284 `FetchInstanceInfoWorker`が原因でSidekiqのJobが詰まる問題
* Fix: InstanceInfoを取得するタイミング
* Fix test
* Fix test
* Fix: HTTPコード
* 調整
---
.../activitypub/process_account_service.rb | 5 +--
.../activitypub/fetch_instance_info_worker.rb | 34 +++++++++----------
.../update_instance_info_scheduler.rb | 15 --------
config/sidekiq.yml | 4 ---
spec/lib/activitypub/activity/update_spec.rb | 1 +
.../process_account_service_spec.rb | 4 +++
.../fetch_instance_info_worker_spec.rb | 20 +++++++++++
.../update_instance_info_scheduler_spec.rb | 19 -----------
8 files changed, 44 insertions(+), 58 deletions(-)
delete mode 100644 app/workers/scheduler/update_instance_info_scheduler.rb
delete mode 100644 spec/workers/scheduler/update_instance_info_scheduler_spec.rb
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 473b2cabd3..23a434ae90 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -46,7 +46,6 @@ class ActivityPub::ProcessAccountService < BaseService
end
create_account
- fetch_instance_info
end
update_account
@@ -66,6 +65,8 @@ class ActivityPub::ProcessAccountService < BaseService
check_links! if @account.fields.any?(&:requires_verification?)
end
+ fetch_instance_info
+
@account
rescue Oj::ParseError
nil
@@ -211,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService
end
def fetch_instance_info
- ActivityPub::FetchInstanceInfoWorker.perform_async(@account.domain) unless InstanceInfo.exists?(domain: @account.domain)
+ ActivityPub::FetchInstanceInfoWorker.perform_async(@account.domain) unless Rails.cache.exist?("fetch_instance_info:#{@account.domain}", expires_in: 1.day)
end
def actor_type
diff --git a/app/workers/activitypub/fetch_instance_info_worker.rb b/app/workers/activitypub/fetch_instance_info_worker.rb
index 57cbd97d10..bc9a1a4815 100644
--- a/app/workers/activitypub/fetch_instance_info_worker.rb
+++ b/app/workers/activitypub/fetch_instance_info_worker.rb
@@ -8,28 +8,32 @@ class ActivityPub::FetchInstanceInfoWorker
sidekiq_options queue: 'push', retry: 2
- class Error < StandardError; end
- class RequestError < Error; end
- class DeadError < Error; end
-
SUPPORTED_NOTEINFO_RELS = ['http://nodeinfo.diaspora.software/ns/schema/2.0', 'http://nodeinfo.diaspora.software/ns/schema/2.1'].freeze
def perform(domain)
@instance = Instance.find_by(domain: domain)
return if !@instance || @instance.unavailable_domain.present?
- with_redis_lock("instance_info:#{domain}") do
- link = nodeinfo_link
- return if link.nil?
-
- update_info!(link)
+ Rails.cache.fetch("fetch_instance_info:#{@instance.domain}", expires_in: 1.day, race_condition_ttl: 1.hour) do
+ fetch!
end
- rescue ActivityPub::FetchInstanceInfoWorker::DeadError
+
true
end
private
+ def fetch!
+ link = nodeinfo_link
+ return if link.nil?
+
+ update_info!(link)
+
+ true
+ rescue Mastodon::UnexpectedResponseError
+ true
+ end
+
def nodeinfo_link
nodeinfo = fetch_json("https://#{@instance.domain}/.well-known/nodeinfo")
return nil if nodeinfo.nil? || !nodeinfo.key?('links')
@@ -63,15 +67,9 @@ class ActivityPub::FetchInstanceInfoWorker
def fetch_json(url)
build_request(url).perform do |response|
- if [200, 203].include?(response.code)
- raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
+ raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response)
- body_to_json(response.body_with_limit)
- elsif [400, 401, 403, 404, 410].include?(response.code)
- raise ActivityPub::FetchInstanceInfoWorker::DeadError, "Request for #{@instance.domain} returned HTTP #{response.code}"
- else
- raise ActivityPub::FetchInstanceInfoWorker::RequestError, "Request for #{@instance.domain} returned HTTP #{response.code}"
- end
+ body_to_json(response.body_with_limit)
end
end
diff --git a/app/workers/scheduler/update_instance_info_scheduler.rb b/app/workers/scheduler/update_instance_info_scheduler.rb
deleted file mode 100644
index f5b2852859..0000000000
--- a/app/workers/scheduler/update_instance_info_scheduler.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-class Scheduler::UpdateInstanceInfoScheduler
- include Sidekiq::Worker
-
- sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.day.to_i
-
- def perform
- Instance.select(:domain).reorder(nil).find_in_batches do |instances|
- ActivityPub::FetchInstanceInfoWorker.push_bulk(instances) do |instance|
- [instance.domain]
- end
- end
- end
-end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index f954c00aaa..b643a1ecfb 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -62,10 +62,6 @@
interval: 30 seconds
class: Scheduler::SidekiqHealthScheduler
queue: scheduler
- update_instance_info_scheduler:
- cron: '0 0 * * *'
- class: Scheduler::UpdateInstanceInfoScheduler
- queue: scheduler
software_update_check_scheduler:
interval: 30 minutes
class: Scheduler::SoftwareUpdateCheckScheduler
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
index 87e96d2d1b..6c84c5836a 100644
--- a/spec/lib/activitypub/activity/update_spec.rb
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -55,6 +55,7 @@ RSpec.describe ActivityPub::Activity::Update do
stub_request(:get, actor_json[:following]).to_return(status: 404)
stub_request(:get, actor_json[:featured]).to_return(status: 404)
stub_request(:get, actor_json[:featuredTags]).to_return(status: 404)
+ stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 404)
subject.perform
end
diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb
index 7810793559..c927b260f7 100644
--- a/spec/services/activitypub/process_account_service_spec.rb
+++ b/spec/services/activitypub/process_account_service_spec.rb
@@ -5,6 +5,10 @@ require 'rails_helper'
RSpec.describe ActivityPub::ProcessAccountService, type: :service do
subject { described_class.new }
+ before do
+ stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 404)
+ end
+
context 'with searchability' do
subject { described_class.new.call('alice', 'example.com', payload) }
diff --git a/spec/workers/activitypub/fetch_instance_info_worker_spec.rb b/spec/workers/activitypub/fetch_instance_info_worker_spec.rb
index f6dacff5fc..9dc9594041 100644
--- a/spec/workers/activitypub/fetch_instance_info_worker_spec.rb
+++ b/spec/workers/activitypub/fetch_instance_info_worker_spec.rb
@@ -67,9 +67,22 @@ describe ActivityPub::FetchInstanceInfoWorker do
Instance.refresh
end
+ it 'does not update immediately' do
+ stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: nodeinfo_json)
+ subject.perform('example.com')
+ stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: new_nodeinfo_json)
+ subject.perform('example.com')
+
+ info = InstanceInfo.find_by(domain: 'example.com')
+ expect(info).to_not be_nil
+ expect(info.software).to eq 'mastodon'
+ expect(info.version).to eq '4.2.0-beta1'
+ end
+
it 'performs a mastodon instance' do
stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: nodeinfo_json)
subject.perform('example.com')
+ Rails.cache.delete('fetch_instance_info:example.com')
stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: new_nodeinfo_json)
subject.perform('example.com')
@@ -93,5 +106,12 @@ describe ActivityPub::FetchInstanceInfoWorker do
info = InstanceInfo.find_by(domain: 'example.com')
expect(info).to be_nil
end
+
+ it 'does not fetch again immediately' do
+ expect(subject.perform('example.com')).to be true
+ expect(subject.perform('example.com')).to be true
+
+ expect(a_request(:get, 'https://example.com/.well-known/nodeinfo')).to have_been_made.once
+ end
end
end
diff --git a/spec/workers/scheduler/update_instance_info_scheduler_spec.rb b/spec/workers/scheduler/update_instance_info_scheduler_spec.rb
deleted file mode 100644
index f3a190417f..0000000000
--- a/spec/workers/scheduler/update_instance_info_scheduler_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Scheduler::UpdateInstanceInfoScheduler do
- let(:worker) { described_class.new }
-
- before do
- stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 200, body: '{}')
- Fabricate(:account, domain: 'example.com')
- Instance.refresh
- end
-
- describe 'perform' do
- it 'runs without error' do
- expect { worker.perform }.to_not raise_error
- end
- end
-end
From 0dec7b450bc417d50f268c49b6b89e124b63a2f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Wed, 13 Dec 2023 09:13:35 +0900
Subject: [PATCH 032/203] =?UTF-8?q?Fix:=20#355=20LTL=E3=81=AE=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=83=87=E3=83=83=E3=82=AF=E3=82=B9=E3=81=8C=E9=81=A9?=
=?UTF-8?q?=E5=88=87=E3=81=AB=E3=81=AF=E3=82=89=E3=82=8C=E3=81=A6=E3=81=84?=
=?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=20(#356)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.rubocop_todo.yml | 1 +
...improve_index_for_public_timeline_speed.rb | 19 +++++++++++++++++++
db/schema.rb | 6 +++---
3 files changed, 23 insertions(+), 3 deletions(-)
create mode 100644 db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 3e93f4432b..eb3d618551 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -130,6 +130,7 @@ Naming/VariableNumber:
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
+ - 'db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/domain_block_spec.rb'
- 'spec/models/user_spec.rb'
diff --git a/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
new file mode 100644
index 0000000000..bcfe83de03
--- /dev/null
+++ b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ImproveIndexForPublicTimelineSpeed < ActiveRecord::Migration[7.0]
+ disable_ddl_transaction!
+
+ def up
+ add_index :statuses, [:id, :account_id], name: :index_statuses_local_20231213, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ add_index :statuses, [:id, :account_id], name: :index_statuses_public_20231213, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ remove_index :statuses, name: :index_statuses_local_20190824
+ remove_index :statuses, name: :index_statuses_public_20200119
+ end
+
+ def down
+ add_index :statuses, [:id, :account_id], name: :index_statuses_local_20190824, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ add_index :statuses, [:id, :account_id], name: :index_statuses_public_20200119, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ remove_index :statuses, name: :index_statuses_local_20231213
+ remove_index :statuses, name: :index_statuses_public_20231213
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 24dc5450e1..f8fd9283e0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_10_07_090808) do
+ActiveRecord::Schema[7.0].define(version: 2023_12_12_225737) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1193,8 +1193,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_07_090808) do
t.index ["account_id", "reblog_of_id", "deleted_at", "searchability"], name: "index_statuses_for_get_following_accounts_to_search", where: "((deleted_at IS NULL) AND (reblog_of_id IS NULL) AND (searchability = ANY (ARRAY[0, 10, 1])))"
t.index ["account_id"], name: "index_statuses_on_account_id"
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
- t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
- t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
+ t.index ["id", "account_id"], name: "index_statuses_local_20231213", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
+ t.index ["id", "account_id"], name: "index_statuses_public_20231213", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
From e9b69478d139b53e9108b7be063e2a93af1ba7a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Wed, 13 Dec 2023 09:13:35 +0900
Subject: [PATCH 033/203] =?UTF-8?q?Fix:=20#355=20LTL=E3=81=AE=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=83=87=E3=83=83=E3=82=AF=E3=82=B9=E3=81=8C=E9=81=A9?=
=?UTF-8?q?=E5=88=87=E3=81=AB=E3=81=AF=E3=82=89=E3=82=8C=E3=81=A6=E3=81=84?=
=?UTF-8?q?=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=20(#356)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.rubocop_todo.yml | 1 +
...improve_index_for_public_timeline_speed.rb | 19 +++++++++++++++++++
db/schema.rb | 6 +++---
3 files changed, 23 insertions(+), 3 deletions(-)
create mode 100644 db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 3e93f4432b..eb3d618551 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -130,6 +130,7 @@ Naming/VariableNumber:
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
+ - 'db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/domain_block_spec.rb'
- 'spec/models/user_spec.rb'
diff --git a/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
new file mode 100644
index 0000000000..bcfe83de03
--- /dev/null
+++ b/db/migrate/20231212225737_improve_index_for_public_timeline_speed.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ImproveIndexForPublicTimelineSpeed < ActiveRecord::Migration[7.0]
+ disable_ddl_transaction!
+
+ def up
+ add_index :statuses, [:id, :account_id], name: :index_statuses_local_20231213, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ add_index :statuses, [:id, :account_id], name: :index_statuses_public_20231213, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility IN (0, 10, 11) AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ remove_index :statuses, name: :index_statuses_local_20190824
+ remove_index :statuses, name: :index_statuses_public_20200119
+ end
+
+ def down
+ add_index :statuses, [:id, :account_id], name: :index_statuses_local_20190824, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ add_index :statuses, [:id, :account_id], name: :index_statuses_public_20200119, algorithm: :concurrently, order: { id: :desc }, where: 'deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ remove_index :statuses, name: :index_statuses_local_20231213
+ remove_index :statuses, name: :index_statuses_public_20231213
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 24dc5450e1..f8fd9283e0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_10_07_090808) do
+ActiveRecord::Schema[7.0].define(version: 2023_12_12_225737) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1193,8 +1193,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_07_090808) do
t.index ["account_id", "reblog_of_id", "deleted_at", "searchability"], name: "index_statuses_for_get_following_accounts_to_search", where: "((deleted_at IS NULL) AND (reblog_of_id IS NULL) AND (searchability = ANY (ARRAY[0, 10, 1])))"
t.index ["account_id"], name: "index_statuses_on_account_id"
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
- t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
- t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
+ t.index ["id", "account_id"], name: "index_statuses_local_20231213", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
+ t.index ["id", "account_id"], name: "index_statuses_public_20231213", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = ANY (ARRAY[0, 10, 11])) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id", where: "(in_reply_to_account_id IS NOT NULL)"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
From e8b6c16b52f97c1f221b7eccb6ed11716e5ff4d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?KMY=EF=BC=88=E9=9B=AA=E3=81=82=E3=81=99=E3=81=8B=EF=BC=89?=
Date: Fri, 15 Dec 2023 09:41:22 +0900
Subject: [PATCH 034/203] Merge pull request from GHSA-qg32-3vrh-w6mw
---
app/controllers/concerns/cache_concern.rb | 10 ++++++++
app/controllers/statuses_controller.rb | 31 ++++++++++++++++++++---
2 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb
index 55ebe1bd64..088b8db56a 100644
--- a/app/controllers/concerns/cache_concern.rb
+++ b/app/controllers/concerns/cache_concern.rb
@@ -180,6 +180,16 @@ module CacheConcern
def render_with_cache(**options)
raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?
+ if options.delete(:cancel_cache)
+ if block_given?
+ options[:json] = yield
+ elsif options[:json].is_a?(Symbol)
+ options[:json] = send(options[:json])
+ end
+
+ return render(options)
+ end
+
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes
body = Rails.cache.read(key, raw: true)
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index 50a8763b72..9ae15a6ed0 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -30,15 +30,15 @@ class StatusesController < ApplicationController
end
format.json do
- expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode?
- render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
+ expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? && !misskey_software?
+ render_with_cache json: @status, content_type: 'application/activity+json', serializer: status_activity_serializer, adapter: ActivityPub::Adapter, cancel_cache: misskey_software?
end
end
end
def activity
- expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
- render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
+ expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? && !misskey_software?
+ render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status, for_misskey: misskey_software?), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, cancel_cache: misskey_software?
end
def embed
@@ -76,6 +76,29 @@ class StatusesController < ApplicationController
@instance_presenter = InstancePresenter.new
end
+ def misskey_software?
+ return @misskey_software if defined?(@misskey_software)
+
+ @misskey_software = false
+
+ return false if !@status.local? || signed_request_account&.domain.blank?
+
+ info = InstanceInfo.find_by(domain: signed_request_account.domain)
+ return false if info.nil?
+
+ @misskey_software = %w(misskey calckey cherrypick sharkey).include?(info.software) &&
+ ((@status.public_unlisted_visibility? && @status.account.user&.setting_reject_public_unlisted_subscription) ||
+ (@status.unlisted_visibility? && @status.account.user&.setting_reject_unlisted_subscription))
+ end
+
+ def status_activity_serializer
+ if misskey_software?
+ ActivityPub::NoteForMisskeySerializer
+ else
+ ActivityPub::NoteSerializer
+ end
+ end
+
def redirect_to_original
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
end
From 38105dfbaa12e16dcb848b440672a24c9c705cd7 Mon Sep 17 00:00:00 2001
From: KMY
Date: Mon, 11 Dec 2023 14:17:00 +0900
Subject: [PATCH 035/203] Bump version to 5.12 LTS
---
lib/mastodon/version.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index e23a4afa94..0e819ce52c 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -9,7 +9,7 @@ module Mastodon
end
def kmyblue_minor
- 11
+ 12
end
def kmyblue_flag
From d7875adad242881876e316477b0610cbad466893 Mon Sep 17 00:00:00 2001
From: Claire
Date: Tue, 19 Dec 2023 11:27:37 +0100
Subject: [PATCH 036/203] Fix call to inefficient `delete_matched` cache method
in domain blocks (#28367)
---
.../api/v1/accounts/notes_controller.rb | 2 +-
.../api/v1/accounts/pins_controller.rb | 2 +-
.../v1/accounts/relationships_controller.rb | 5 +-
app/controllers/api/v1/accounts_controller.rb | 2 +-
.../api/v1/follow_requests_controller.rb | 4 +-
app/controllers/relationships_controller.rb | 2 +-
app/models/account_domain_block.rb | 10 +--
app/models/concerns/account_interactions.rb | 6 --
app/models/concerns/relationship_cacheable.rb | 4 +-
.../account_relationships_presenter.rb | 63 ++++++++++++++-----
.../account_relationships_presenter_spec.rb | 61 +++++++++++++-----
11 files changed, 104 insertions(+), 57 deletions(-)
diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb
index 032e807d11..6d115631a2 100644
--- a/app/controllers/api/v1/accounts/notes_controller.rb
+++ b/app/controllers/api/v1/accounts/notes_controller.rb
@@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController
end
def relationships_presenter
- AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
+ AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
end
diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb
index 73f845c614..0eb13c048c 100644
--- a/app/controllers/api/v1/accounts/pins_controller.rb
+++ b/app/controllers/api/v1/accounts/pins_controller.rb
@@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
end
def relationships_presenter
- AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
+ AccountRelationshipsPresenter.new([@account], current_user.account_id)
end
end
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb
index 503f85c97d..038d6700f5 100644
--- a/app/controllers/api/v1/accounts/relationships_controller.rb
+++ b/app/controllers/api/v1/accounts/relationships_controller.rb
@@ -5,11 +5,10 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user!
def index
- accounts = Account.without_suspended.where(id: account_ids).select('id')
+ @accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a
# .where doesn't guarantee that our results are in the same order
# we requested them, so return the "right" order to the requestor.
- @accounts = accounts.index_by(&:id).values_at(*account_ids).compact
- render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
+ render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships
end
private
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index ddb94d5ca4..321a57ac57 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -86,7 +86,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def relationships(**options)
- AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
+ AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
end
def account_params
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index 7c197ce6ba..ee717ebbcc 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController
private
def account
- Account.find(params[:id])
+ @account ||= Account.find(params[:id])
end
def relationships(**options)
- AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
+ AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
end
def load_accounts
diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb
index e87b5a656f..dd794f3199 100644
--- a/app/controllers/relationships_controller.rb
+++ b/app/controllers/relationships_controller.rb
@@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController
end
def set_relationships
- @relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id)
+ @relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
end
def form_account_batch_params
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
index af1e6a68dc..db2e37184f 100644
--- a/app/models/account_domain_block.rb
+++ b/app/models/account_domain_block.rb
@@ -18,16 +18,12 @@ class AccountDomainBlock < ApplicationRecord
belongs_to :account
validates :domain, presence: true, uniqueness: { scope: :account_id }, domain: true
- after_commit :remove_blocking_cache
- after_commit :remove_relationship_cache
+ after_commit :invalidate_domain_blocking_cache
private
- def remove_blocking_cache
+ def invalidate_domain_blocking_cache
Rails.cache.delete("exclude_domains_for:#{account_id}")
- end
-
- def remove_relationship_cache
- Rails.cache.delete_matched("relationship:#{account_id}:*")
+ Rails.cache.delete(['exclude_domains', account_id, domain])
end
end
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
index 3c64ebd9fa..2de15031f1 100644
--- a/app/models/concerns/account_interactions.rb
+++ b/app/models/concerns/account_interactions.rb
@@ -60,12 +60,6 @@ module AccountInteractions
end
end
- def domain_blocking_map(target_account_ids, account_id)
- accounts_map = Account.where(id: target_account_ids).select('id, domain').each_with_object({}) { |a, h| h[a.id] = a.domain }
- blocked_domains = domain_blocking_map_by_domain(accounts_map.values.compact, account_id)
- accounts_map.reduce({}) { |h, (id, domain)| h.merge(id => blocked_domains[domain]) }
- end
-
def domain_blocking_map_by_domain(target_domains, account_id)
follow_mapping(AccountDomainBlock.where(account_id: account_id, domain: target_domains), :domain)
end
diff --git a/app/models/concerns/relationship_cacheable.rb b/app/models/concerns/relationship_cacheable.rb
index 0d9359f7e7..c32a8d62c6 100644
--- a/app/models/concerns/relationship_cacheable.rb
+++ b/app/models/concerns/relationship_cacheable.rb
@@ -10,7 +10,7 @@ module RelationshipCacheable
private
def remove_relationship_cache
- Rails.cache.delete("relationship:#{account_id}:#{target_account_id}")
- Rails.cache.delete("relationship:#{target_account_id}:#{account_id}")
+ Rails.cache.delete(['relationship', account_id, target_account_id])
+ Rails.cache.delete(['relationship', target_account_id, account_id])
end
end
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index 5d2b5435dd..8482ef54da 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -5,8 +5,9 @@ class AccountRelationshipsPresenter
:muting, :requested, :requested_by, :domain_blocking,
:endorsed, :account_note
- def initialize(account_ids, current_account_id, **options)
- @account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a.to_i }
+ def initialize(accounts, current_account_id, **options)
+ @accounts = accounts.to_a
+ @account_ids = @accounts.pluck(:id)
@current_account_id = current_account_id
@following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id))
@@ -16,10 +17,11 @@ class AccountRelationshipsPresenter
@muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
@requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
@requested_by = cached[:requested_by].merge(Account.requested_by_map(@uncached_account_ids, @current_account_id))
- @domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
@endorsed = cached[:endorsed].merge(Account.endorsed_map(@uncached_account_ids, @current_account_id))
@account_note = cached[:account_note].merge(Account.account_note_map(@uncached_account_ids, @current_account_id))
+ @domain_blocking = domain_blocking_map
+
cache_uncached!
@following.merge!(options[:following_map] || {})
@@ -36,6 +38,31 @@ class AccountRelationshipsPresenter
private
+ def domain_blocking_map
+ target_domains = @accounts.pluck(:domain).compact.uniq
+ blocks_by_domain = {}
+
+ # Fetch from cache
+ cache_keys = target_domains.map { |domain| domain_cache_key(domain) }
+ Rails.cache.read_multi(*cache_keys).each do |key, blocking|
+ blocks_by_domain[key.last] = blocking
+ end
+
+ uncached_domains = target_domains - blocks_by_domain.keys
+
+ # Read uncached values from database
+ AccountDomainBlock.where(account_id: @current_account_id, domain: uncached_domains).pluck(:domain).each do |domain|
+ blocks_by_domain[domain] = true
+ end
+
+ # Write database reads to cache
+ to_cache = uncached_domains.to_h { |domain| [domain_cache_key(domain), blocks_by_domain[domain]] }
+ Rails.cache.write_multi(to_cache, expires_in: 1.day)
+
+ # Return formatted value
+ @accounts.each_with_object({}) { |account, h| h[account.id] = blocks_by_domain[account.domain] }
+ end
+
def cached
return @cached if defined?(@cached)
@@ -47,28 +74,23 @@ class AccountRelationshipsPresenter
muting: {},
requested: {},
requested_by: {},
- domain_blocking: {},
endorsed: {},
account_note: {},
}
- @uncached_account_ids = []
+ @uncached_account_ids = @account_ids.uniq
- @account_ids.each do |account_id|
- maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}")
-
- if maps_for_account.is_a?(Hash)
- @cached.deep_merge!(maps_for_account)
- else
- @uncached_account_ids << account_id
- end
+ cache_ids = @account_ids.map { |account_id| relationship_cache_key(account_id) }
+ Rails.cache.read_multi(*cache_ids).each do |key, maps_for_account|
+ @cached.deep_merge!(maps_for_account)
+ @uncached_account_ids.delete(key.last)
end
@cached
end
def cache_uncached!
- @uncached_account_ids.each do |account_id|
+ to_cache = @uncached_account_ids.to_h do |account_id|
maps_for_account = {
following: { account_id => following[account_id] },
followed_by: { account_id => followed_by[account_id] },
@@ -77,12 +99,21 @@ class AccountRelationshipsPresenter
muting: { account_id => muting[account_id] },
requested: { account_id => requested[account_id] },
requested_by: { account_id => requested_by[account_id] },
- domain_blocking: { account_id => domain_blocking[account_id] },
endorsed: { account_id => endorsed[account_id] },
account_note: { account_id => account_note[account_id] },
}
- Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day)
+ [relationship_cache_key(account_id), maps_for_account]
end
+
+ Rails.cache.write_multi(to_cache, expires_in: 1.day)
+ end
+
+ def domain_cache_key(domain)
+ ['exclude_domains', @current_account_id, domain]
+ end
+
+ def relationship_cache_key(account_id)
+ ['relationship', @current_account_id, account_id]
end
end
diff --git a/spec/presenters/account_relationships_presenter_spec.rb b/spec/presenters/account_relationships_presenter_spec.rb
index 5c2ba54e00..282cae4f06 100644
--- a/spec/presenters/account_relationships_presenter_spec.rb
+++ b/spec/presenters/account_relationships_presenter_spec.rb
@@ -5,30 +5,57 @@ require 'rails_helper'
RSpec.describe AccountRelationshipsPresenter do
describe '.initialize' do
before do
- allow(Account).to receive(:following_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:followed_by_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:blocking_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:muting_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:requested_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:requested_by_map).with(account_ids, current_account_id).and_return(default_map)
- allow(Account).to receive(:domain_blocking_map).with(account_ids, current_account_id).and_return(default_map)
+ allow(Account).to receive(:following_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
+ allow(Account).to receive(:followed_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
+ allow(Account).to receive(:blocking_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
+ allow(Account).to receive(:muting_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
+ allow(Account).to receive(:requested_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
+ allow(Account).to receive(:requested_by_map).with(accounts.pluck(:id), current_account_id).and_return(default_map)
end
- let(:presenter) { described_class.new(account_ids, current_account_id, **options) }
+ let(:presenter) { described_class.new(accounts, current_account_id, **options) }
let(:current_account_id) { Fabricate(:account).id }
- let(:account_ids) { [Fabricate(:account).id] }
- let(:default_map) { { 1 => true } }
+ let(:accounts) { [Fabricate(:account)] }
+ let(:default_map) { { accounts[0].id => true } }
context 'when options are not set' do
let(:options) { {} }
it 'sets default maps' do
- expect(presenter.following).to eq default_map
- expect(presenter.followed_by).to eq default_map
- expect(presenter.blocking).to eq default_map
- expect(presenter.muting).to eq default_map
- expect(presenter.requested).to eq default_map
- expect(presenter.domain_blocking).to eq default_map
+ expect(presenter).to have_attributes(
+ following: default_map,
+ followed_by: default_map,
+ blocking: default_map,
+ muting: default_map,
+ requested: default_map,
+ domain_blocking: { accounts[0].id => nil }
+ )
+ end
+ end
+
+ context 'with a warm cache' do
+ let(:options) { {} }
+
+ before do
+ described_class.new(accounts, current_account_id, **options)
+
+ allow(Account).to receive(:following_map).with([], current_account_id).and_return({})
+ allow(Account).to receive(:followed_by_map).with([], current_account_id).and_return({})
+ allow(Account).to receive(:blocking_map).with([], current_account_id).and_return({})
+ allow(Account).to receive(:muting_map).with([], current_account_id).and_return({})
+ allow(Account).to receive(:requested_map).with([], current_account_id).and_return({})
+ allow(Account).to receive(:requested_by_map).with([], current_account_id).and_return({})
+ end
+
+ it 'sets returns expected values' do
+ expect(presenter).to have_attributes(
+ following: default_map,
+ followed_by: default_map,
+ blocking: default_map,
+ muting: default_map,
+ requested: default_map,
+ domain_blocking: { accounts[0].id => nil }
+ )
end
end
@@ -84,7 +111,7 @@ RSpec.describe AccountRelationshipsPresenter do
let(:options) { { domain_blocking_map: { 7 => true } } }
it 'sets @domain_blocking merged with default_map and options[:domain_blocking_map]' do
- expect(presenter.domain_blocking).to eq default_map.merge(options[:domain_blocking_map])
+ expect(presenter.domain_blocking).to eq({ accounts[0].id => nil }.merge(options[:domain_blocking_map]))
end
end
end
From 06123147d51b64ab5ae09dcc7d69f20de414cb69 Mon Sep 17 00:00:00 2001
From: KMY
Date: Thu, 4 Jan 2024 15:32:58 +0900
Subject: [PATCH 037/203] =?UTF-8?q?Fix:=20=E3=82=A2=E3=83=B3=E3=83=86?=
=?UTF-8?q?=E3=83=8A=E3=81=AB=E7=99=BB=E9=8C=B2=E3=81=95=E3=82=8C=E3=81=9F?=
=?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=81=8C=E3=82=A2=E3=83=B3=E3=83=86=E3=83=8A?=
=?UTF-8?q?=E5=89=8A=E9=99=A4=E6=99=82Redis=E3=81=8B=E3=82=89=E5=89=8A?=
=?UTF-8?q?=E9=99=A4=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?=
=?UTF-8?q?=20(#417)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix: アンテナに登録された投稿がRedisから削除されない問題
* Fix test
* Tootctlに変更
* 処理を共通化
---
app/lib/vacuum/feeds_vacuum.rb | 11 ++
app/models/antenna.rb | 8 +
lib/mastodon/cli/feeds.rb | 25 +++
spec/lib/vacuum/feeds_vacuum_spec.rb | 14 ++
spec/requests/api/v1/antennas_spec.rb | 234 ++++++++++++++++++++++++++
5 files changed, 292 insertions(+)
create mode 100644 spec/requests/api/v1/antennas_spec.rb
diff --git a/app/lib/vacuum/feeds_vacuum.rb b/app/lib/vacuum/feeds_vacuum.rb
index fb0b8a8472..a9e96accdd 100644
--- a/app/lib/vacuum/feeds_vacuum.rb
+++ b/app/lib/vacuum/feeds_vacuum.rb
@@ -4,6 +4,7 @@ class Vacuum::FeedsVacuum
def perform
vacuum_inactive_home_feeds!
vacuum_inactive_list_feeds!
+ vacuum_inactive_antenna_feeds!
end
private
@@ -20,6 +21,12 @@ class Vacuum::FeedsVacuum
end
end
+ def vacuum_inactive_antenna_feeds!
+ inactive_users_antennas.select(:id).in_batches do |antennas|
+ feed_manager.clean_feeds!(:antenna, antennas.ids)
+ end
+ end
+
def inactive_users
User.confirmed.inactive
end
@@ -28,6 +35,10 @@ class Vacuum::FeedsVacuum
List.where(account_id: inactive_users.select(:account_id))
end
+ def inactive_users_antennas
+ Antenna.where(account_id: inactive_users.select(:account_id))
+ end
+
def feed_manager
FeedManager.instance
end
diff --git a/app/models/antenna.rb b/app/models/antenna.rb
index a35fa6dd17..c64e66bde2 100644
--- a/app/models/antenna.rb
+++ b/app/models/antenna.rb
@@ -55,11 +55,15 @@ class Antenna < ApplicationRecord
scope :available_stls, -> { where(available: true, stl: true) }
scope :available_ltls, -> { where(available: true, stl: false, ltl: true) }
+ validates :title, presence: true
+
validate :list_owner
validate :validate_limit
validate :validate_stl_limit
validate :validate_ltl_limit
+ before_destroy :clean_feed_manager
+
def list_owner
raise Mastodon::ValidationError, I18n.t('antennas.errors.invalid_list_owner') if !list_id.zero? && list.present? && list.account != account
end
@@ -121,4 +125,8 @@ class Antenna < ApplicationRecord
ltls.any? { |tl| !tl.insert_feeds }
end
end
+
+ def clean_feed_manager
+ FeedManager.instance.clean_feeds!(:antenna, [id])
+ end
end
diff --git a/lib/mastodon/cli/feeds.rb b/lib/mastodon/cli/feeds.rb
index 3467dd427c..4f2de4ebb4 100644
--- a/lib/mastodon/cli/feeds.rb
+++ b/lib/mastodon/cli/feeds.rb
@@ -48,10 +48,35 @@ module Mastodon::CLI
say('OK', :green)
end
+ desc 'remove_legacy', 'Remove old list and antenna feeds from Redis'
+ def remove_legacy
+ current_id = 1
+ List.reorder(:id).select(:id).find_in_batches do |lists|
+ current_id = remove_legacy_feeds(:list, lists, current_id)
+ end
+
+ current_id = 1
+ Antenna.reorder(:id).select(:id).find_in_batches do |antennas|
+ current_id = remove_legacy_feeds(:antenna, antennas, current_id)
+ end
+
+ say('OK', :green)
+ end
+
private
def active_user_accounts
Account.joins(:user).merge(User.active)
end
+
+ def remove_legacy_feeds(type, items, current_id)
+ exist_ids = items.pluck(:id)
+ last_id = exist_ids.max
+
+ ids = Range.new(current_id, last_id).to_a - exist_ids
+ FeedManager.instance.clean_feeds!(type, ids)
+
+ last_id + 1
+ end
end
end
diff --git a/spec/lib/vacuum/feeds_vacuum_spec.rb b/spec/lib/vacuum/feeds_vacuum_spec.rb
index ede1e3c360..fa5381c8ae 100644
--- a/spec/lib/vacuum/feeds_vacuum_spec.rb
+++ b/spec/lib/vacuum/feeds_vacuum_spec.rb
@@ -8,12 +8,16 @@ RSpec.describe Vacuum::FeedsVacuum do
describe '#perform' do
let!(:active_user) { Fabricate(:user, current_sign_in_at: 2.days.ago) }
let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) }
+ let!(:list) { Fabricate(:list, account: inactive_user.account) }
+ let!(:antenna) { Fabricate(:antenna, account: inactive_user.account) }
before do
redis.zadd(feed_key_for(inactive_user), 1, 1)
redis.zadd(feed_key_for(active_user), 1, 1)
redis.zadd(feed_key_for(inactive_user, 'reblogs'), 2, 2)
redis.sadd(feed_key_for(inactive_user, 'reblogs:2'), 3)
+ redis.zadd(list_key_for(list), 1, 1)
+ redis.zadd(antenna_key_for(antenna), 1, 1)
subject.perform
end
@@ -23,10 +27,20 @@ RSpec.describe Vacuum::FeedsVacuum do
expect(redis.zcard(feed_key_for(active_user))).to eq 1
expect(redis.exists?(feed_key_for(inactive_user, 'reblogs'))).to be false
expect(redis.exists?(feed_key_for(inactive_user, 'reblogs:2'))).to be false
+ expect(redis.zcard(list_key_for(list))).to eq 0
+ expect(redis.zcard(antenna_key_for(antenna))).to eq 0
end
end
def feed_key_for(user, subtype = nil)
FeedManager.instance.key(:home, user.account_id, subtype)
end
+
+ def list_key_for(list)
+ FeedManager.instance.key(:list, list.id)
+ end
+
+ def antenna_key_for(antenna)
+ FeedManager.instance.key(:antenna, antenna.id)
+ end
end
diff --git a/spec/requests/api/v1/antennas_spec.rb b/spec/requests/api/v1/antennas_spec.rb
new file mode 100644
index 0000000000..1597bbd81f
--- /dev/null
+++ b/spec/requests/api/v1/antennas_spec.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Antennas' do
+ let(:user) { Fabricate(:user) }
+ let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
+ let(:scopes) { 'read:lists write:lists' }
+ let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
+
+ describe 'GET /api/v1/antennas' do
+ subject do
+ get '/api/v1/antennas', headers: headers
+ end
+
+ let!(:antennas) do
+ [
+ Fabricate(:antenna, account: user.account, title: 'first antenna'),
+ Fabricate(:antenna, account: user.account, title: 'second antenna', with_media_only: true),
+ Fabricate(:antenna, account: user.account, title: 'third antenna', stl: true),
+ Fabricate(:antenna, account: user.account, title: 'fourth antenna', ignore_reblog: true),
+ ]
+ end
+
+ let(:expected_response) do
+ antennas.map do |antenna|
+ {
+ id: antenna.id.to_s,
+ title: antenna.title,
+ with_media_only: antenna.with_media_only,
+ ignore_reblog: antenna.ignore_reblog,
+ stl: antenna.stl,
+ ltl: antenna.ltl,
+ insert_feeds: antenna.insert_feeds,
+ list: nil,
+ accounts_count: 0,
+ domains_count: 0,
+ tags_count: 0,
+ keywords_count: 0,
+ }
+ end
+ end
+
+ before do
+ Fabricate(:antenna)
+ end
+
+ it_behaves_like 'forbidden for wrong scope', 'write write:lists'
+
+ it 'returns the expected antennas', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(body_as_json).to match_array(expected_response)
+ end
+ end
+
+ describe 'GET /api/v1/antennas/:id' do
+ subject do
+ get "/api/v1/antennas/#{antenna.id}", headers: headers
+ end
+
+ let(:antenna) { Fabricate(:antenna, account: user.account) }
+
+ it_behaves_like 'forbidden for wrong scope', 'write write:lists'
+
+ it 'returns the requested antenna correctly', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(body_as_json).to eq({
+ id: antenna.id.to_s,
+ title: antenna.title,
+ with_media_only: antenna.with_media_only,
+ ignore_reblog: antenna.ignore_reblog,
+ stl: antenna.stl,
+ ltl: antenna.ltl,
+ insert_feeds: antenna.insert_feeds,
+ list: nil,
+ accounts_count: 0,
+ domains_count: 0,
+ tags_count: 0,
+ keywords_count: 0,
+ })
+ end
+
+ context 'when the antenna belongs to a different user' do
+ let(:antenna) { Fabricate(:antenna) }
+
+ it 'returns http not found' do
+ subject
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the antenna does not exist' do
+ it 'returns http not found' do
+ get '/api/v1/antennas/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /api/v1/antennas' do
+ subject do
+ post '/api/v1/antennas', headers: headers, params: params
+ end
+
+ let(:params) { { title: 'my antenna', ltl: 'true' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'read read:lists'
+
+ it 'returns the new antenna', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(body_as_json).to match(a_hash_including(title: 'my antenna', ltl: true))
+ expect(Antenna.where(account: user.account).count).to eq(1)
+ end
+
+ context 'when a title is not given' do
+ let(:params) { { title: '' } }
+
+ it 'returns http unprocessable entity' do
+ subject
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'PUT /api/v1/antennas/:id' do
+ subject do
+ put "/api/v1/antennas/#{antenna.id}", headers: headers, params: params
+ end
+
+ let(:antenna) { Fabricate(:antenna, account: user.account, title: 'my antenna') }
+ let(:params) { { title: 'antenna', ignore_reblog: 'true', insert_feeds: 'true' } }
+
+ it_behaves_like 'forbidden for wrong scope', 'read read:lists'
+
+ it 'returns the updated antenna and updates values', :aggregate_failures do
+ expect { subject }
+ .to change_antenna_title
+ .and change_antenna_ignore_reblog
+ .and change_antenna_insert_feeds
+
+ expect(response).to have_http_status(200)
+ antenna.reload
+
+ expect(body_as_json).to eq({
+ id: antenna.id.to_s,
+ title: antenna.title,
+ with_media_only: antenna.with_media_only,
+ ignore_reblog: antenna.ignore_reblog,
+ stl: antenna.stl,
+ ltl: antenna.ltl,
+ insert_feeds: antenna.insert_feeds,
+ list: nil,
+ accounts_count: 0,
+ domains_count: 0,
+ tags_count: 0,
+ keywords_count: 0,
+ })
+ end
+
+ def change_antenna_title
+ change { antenna.reload.title }.from('my antenna').to('antenna')
+ end
+
+ def change_antenna_ignore_reblog
+ change { antenna.reload.ignore_reblog }.from(false).to(true)
+ end
+
+ def change_antenna_insert_feeds
+ change { antenna.reload.insert_feeds }.from(false).to(true)
+ end
+
+ context 'when the antenna does not exist' do
+ it 'returns http not found' do
+ put '/api/v1/antennas/-1', headers: headers, params: params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the antenna belongs to another user' do
+ let(:antenna) { Fabricate(:antenna) }
+
+ it 'returns http not found' do
+ subject
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /api/v1/antennas/:id' do
+ subject do
+ delete "/api/v1/antennas/#{antenna.id}", headers: headers
+ end
+
+ let(:antenna) { Fabricate(:antenna, account: user.account) }
+
+ it_behaves_like 'forbidden for wrong scope', 'read read:lists'
+
+ it 'deletes the antenna', :aggregate_failures do
+ subject
+
+ expect(response).to have_http_status(200)
+ expect(Antenna.where(id: antenna.id)).to_not exist
+ end
+
+ context 'when the antenna does not exist' do
+ it 'returns http not found' do
+ delete '/api/v1/antennas/-1', headers: headers
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the antenna belongs to another user' do
+ let(:antenna) { Fabricate(:antenna) }
+
+ it 'returns http not found' do
+ subject
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
From 267e9cbcc86691b828f1ebe4abe9505f38b9cdc6 Mon Sep 17 00:00:00 2001
From: KMY
Date: Sun, 7 Jan 2024 16:02:41 +0900
Subject: [PATCH 038/203] =?UTF-8?q?Remove:=20#429=20=E3=83=89=E3=83=A1?=
=?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E3=81=AE?=
=?UTF-8?q?=E3=80=8C=E6=9C=AA=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=83=A6?=
=?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AB=E9=9D=9E=E5=85=AC=E9=96=8B?=
=?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=E3=80=8D=E3=82=AA=E3=83=97=E3=82=B7?=
=?UTF-8?q?=E3=83=A7=E3=83=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/controllers/admin/domain_blocks_controller.rb | 6 +++---
.../api/v1/instances/domain_blocks_controller.rb | 1 -
app/models/domain_block.rb | 2 +-
app/views/admin/domain_blocks/edit.html.haml | 3 ---
app/views/admin/domain_blocks/new.html.haml | 3 ---
config/locales/ja.yml | 2 --
6 files changed, 4 insertions(+), 13 deletions(-)
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index c91b9b7163..ed8f425de9 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -89,17 +89,17 @@ module Admin
def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
- :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
+ :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
- :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
+ :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
def form_domain_block_batch_params
params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media,
- :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous])
+ :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden])
end
def action_from_button
diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
index e91f48e1db..c91234e088 100644
--- a/app/controllers/api/v1/instances/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -26,6 +26,5 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
def set_domain_blocks
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
- @domain_blocks = @domain_blocks.filter { |block| !block.hidden_anonymous } unless user_signed_in?
end
end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index b05fa19476..9f282faf75 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -43,7 +43,7 @@ class DomainBlock < ApplicationRecord
delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
- scope :with_user_facing_limitations, -> { where(hidden: false) }
+ scope :with_user_facing_limitations, -> { where(hidden: false, hidden_anonymous: false) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)).or(where(reject_favourite: true)).or(where(reject_reply: true)).or(where(reject_reply_exclude_followers: true)).or(where(reject_new_follow: true)).or(where(reject_straight_follow: true)) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) }
diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml
index 8a06441508..4a59178c08 100644
--- a/app/views/admin/domain_blocks/edit.html.haml
+++ b/app/views/admin/domain_blocks/edit.html.haml
@@ -65,8 +65,5 @@
.fields-group
= f.input :hidden, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden'), hint: I18n.t('admin.domain_blocks.hidden_hint')
- .fields-group
- = f.input :hidden_anonymous, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint')
-
.actions
= f.button :button, t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index 606a784e12..bb64b515fd 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -65,8 +65,5 @@
.fields-group
= f.input :hidden, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden'), hint: I18n.t('admin.domain_blocks.hidden_hint')
- .fields-group
- = f.input :hidden_anonymous, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint')
-
.actions
= f.button :button, t('.create'), type: :submit
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 85cb345e21..89684ce637 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -400,8 +400,6 @@ ja:
export: エクスポート
hidden: 非公開にする
hidden_hint: 公開することで当サーバーの安全が脅かされる場合、このドメインブロックを非公開にすることができます。
- hidden_anonymous: 未ログインユーザーに非公開にする
- hidden_anonymous_hint: 公開することで当サーバーの安全が脅かされる場合、非ログインユーザーに限りこのドメインブロックを非公開にすることができます。
import: インポート
new:
create: ブロックを作成
From 7edb05337ee02ed6efbf32451153ff5e20b2f135 Mon Sep 17 00:00:00 2001
From: KMY
Date: Tue, 9 Jan 2024 13:08:48 +0900
Subject: [PATCH 039/203] =?UTF-8?q?Add:=20#437=20=E3=83=89=E3=83=A1?=
=?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E3=81=A7?=
=?UTF-8?q?=E3=80=8C=E6=9C=AA=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=83=A6?=
=?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AB=E9=9D=9E=E5=85=AC=E9=96=8B?=
=?UTF-8?q?=E3=80=8D=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=82=92=E3=80=8C=E9=9D=9E?=
=?UTF-8?q?=E5=85=AC=E9=96=8B=E3=80=8D=E3=81=AB=E3=82=B3=E3=83=94=E3=83=BC?=
=?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=9E=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC?=
=?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.rubocop_todo.yml | 1 +
.../api/v1/admin/domain_blocks_controller.rb | 4 ++--
app/models/domain_block.rb | 3 +--
...move_hidden_anonymous_from_domain_blocks.rb | 18 ++++++++++++++++++
db/schema.rb | 3 +--
5 files changed, 23 insertions(+), 6 deletions(-)
create mode 100644 db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index eb3d618551..831e518af5 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -442,6 +442,7 @@ Rails/SkipsModelValidations:
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/migrate/20191007013357_update_pt_locales.rb'
- 'db/migrate/20220316233212_update_kurdish_locales.rb'
+ - 'db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb'
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
- 'db/post_migrate/20200917193528_migrate_notifications_type.rb'
- 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb'
diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb
index bd0660dbaa..c1a45145cb 100644
--- a/app/controllers/api/v1/admin/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb
@@ -70,7 +70,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def domain_block_params
params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
- :reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
+ :reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden)
end
def insert_pagination_headers
@@ -103,6 +103,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
def resource_params
params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
- :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous)
+ :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 9f282faf75..37703fec99 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -25,7 +25,6 @@
# reject_straight_follow :boolean default(FALSE), not null
# reject_new_follow :boolean default(FALSE), not null
# hidden :boolean default(FALSE), not null
-# hidden_anonymous :boolean default(FALSE), not null
# detect_invalid_subscription :boolean default(FALSE), not null
# reject_reply_exclude_followers :boolean default(FALSE), not null
#
@@ -43,7 +42,7 @@ class DomainBlock < ApplicationRecord
delegate :count, to: :accounts, prefix: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
- scope :with_user_facing_limitations, -> { where(hidden: false, hidden_anonymous: false) }
+ scope :with_user_facing_limitations, -> { where(hidden: false) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)).or(where(reject_favourite: true)).or(where(reject_reply: true)).or(where(reject_reply_exclude_followers: true)).or(where(reject_new_follow: true)).or(where(reject_straight_follow: true)) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) }
diff --git a/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb b/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
new file mode 100644
index 0000000000..ef602f03b9
--- /dev/null
+++ b/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class RemoveHiddenAnonymousFromDomainBlocks < ActiveRecord::Migration[7.1]
+ class DomainBlock < ApplicationRecord; end
+
+ def up
+ safety_assured do
+ DomainBlock.where(hidden_anonymous: true, hidden: false).update_all(hidden: true)
+ remove_column :domain_blocks, :hidden_anonymous
+ end
+ end
+
+ def down
+ safety_assured do
+ add_column :domain_blocks, :hidden_anonymous, :boolean, null: false, default: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f8fd9283e0..49bcce1425 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_12_12_225737) do
+ActiveRecord::Schema[7.1].define(version: 2024_01_09_035435) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -570,7 +570,6 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_12_225737) do
t.boolean "reject_straight_follow", default: false, null: false
t.boolean "reject_new_follow", default: false, null: false
t.boolean "hidden", default: false, null: false
- t.boolean "hidden_anonymous", default: false, null: false
t.boolean "detect_invalid_subscription", default: false, null: false
t.boolean "reject_reply_exclude_followers", default: false, null: false
t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
From e17ddc1b6a37174a498910f388e3b8c78a4349b3 Mon Sep 17 00:00:00 2001
From: KMY
Date: Tue, 9 Jan 2024 13:15:45 +0900
Subject: [PATCH 040/203] Fix test
---
...20240109035435_remove_hidden_anonymous_from_domain_blocks.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb b/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
index ef602f03b9..d1272cd79f 100644
--- a/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
+++ b/db/migrate/20240109035435_remove_hidden_anonymous_from_domain_blocks.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class RemoveHiddenAnonymousFromDomainBlocks < ActiveRecord::Migration[7.1]
+class RemoveHiddenAnonymousFromDomainBlocks < ActiveRecord::Migration[7.0]
class DomainBlock < ApplicationRecord; end
def up
From 3e4bd8332658bc6cb892434f411e72091f25e4ff Mon Sep 17 00:00:00 2001
From: KMY
Date: Tue, 9 Jan 2024 13:25:32 +0900
Subject: [PATCH 041/203] Fix test
---
db/schema.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/db/schema.rb b/db/schema.rb
index 49bcce1425..8542eb9450 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2024_01_09_035435) do
+ActiveRecord::Schema[7.0].define(version: 2024_01_09_035435) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
From aa5e50e5d5289325a599365bc920daed266ca477 Mon Sep 17 00:00:00 2001
From: KMY
Date: Tue, 9 Jan 2024 15:57:33 +0900
Subject: [PATCH 042/203] =?UTF-8?q?Change:=20NodeInfo=E3=81=AE=E3=83=90?=
=?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E8=A1=A8=E8=A8=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/serializers/nodeinfo/serializer.rb | 6 +++++-
lib/mastodon/version.rb | 13 ++++++++++++-
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/app/serializers/nodeinfo/serializer.rb b/app/serializers/nodeinfo/serializer.rb
index 3555f0bd8d..b316753c72 100644
--- a/app/serializers/nodeinfo/serializer.rb
+++ b/app/serializers/nodeinfo/serializer.rb
@@ -11,7 +11,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer
end
def software
- { name: 'mastodon', version: Mastodon::Version.to_s }
+ { name: 'kmyblue', version: Mastodon::Version.to_s }
end
def services
@@ -41,6 +41,10 @@ class NodeInfo::Serializer < ActiveModel::Serializer
def metadata
{
features: fedibird_capabilities,
+ upstream: {
+ name: 'Mastodon',
+ version: Mastodon::Version.to_s_of_mastodon,
+ },
}
end
diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb
index 0e819ce52c..0d3e40b859 100644
--- a/lib/mastodon/version.rb
+++ b/lib/mastodon/version.rb
@@ -46,8 +46,19 @@ module Mastodon
components.join
end
+ def to_s_of_mastodon
+ components = [to_a.join('.')]
+ components << "-#{prerelease}" if prerelease.present?
+ components << "+#{build_metadata_of_mastodon}" if build_metadata_of_mastodon.present?
+ components.join
+ end
+
def build_metadata
- ['kmyblue', to_s_of_kmyblue, ENV.fetch('MASTODON_VERSION_METADATA', nil)].compact.join('.')
+ ['kmyblue', to_s_of_kmyblue, build_metadata_of_mastodon].compact.join('.')
+ end
+
+ def build_metadata_of_mastodon
+ ENV.fetch('MASTODON_VERSION_METADATA', nil)
end
def to_a
From 4b10bf23ab240202eb8034729311679e11f1048a Mon Sep 17 00:00:00 2001
From: KMY
Date: Tue, 9 Jan 2024 18:36:11 +0900
Subject: [PATCH 043/203] Remove: SidekiqHealthScheduler
---
app/workers/scheduler/sidekiq_health_scheduler.rb | 12 ------------
config/sidekiq.yml | 4 ----
.../scheduler/sidekiq_health_scheduler_spec.rb | 13 -------------
3 files changed, 29 deletions(-)
delete mode 100644 app/workers/scheduler/sidekiq_health_scheduler.rb
delete mode 100644 spec/workers/scheduler/sidekiq_health_scheduler_spec.rb
diff --git a/app/workers/scheduler/sidekiq_health_scheduler.rb b/app/workers/scheduler/sidekiq_health_scheduler.rb
deleted file mode 100644
index 646976d831..0000000000
--- a/app/workers/scheduler/sidekiq_health_scheduler.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class Scheduler::SidekiqHealthScheduler
- include Sidekiq::Worker
-
- sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 15.seconds.to_i
-
- def perform
- url = ENV.fetch('SIDEKIQ_HEALTH_FETCH_URL', nil)
- Request.new(:head, url).perform if url.present?
- end
-end
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index b643a1ecfb..f1ba5651dd 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -58,10 +58,6 @@
interval: 1 minute
class: Scheduler::SuspendedUserCleanupScheduler
queue: scheduler
- sidekiq_health_scheduler:
- interval: 30 seconds
- class: Scheduler::SidekiqHealthScheduler
- queue: scheduler
software_update_check_scheduler:
interval: 30 minutes
class: Scheduler::SoftwareUpdateCheckScheduler
diff --git a/spec/workers/scheduler/sidekiq_health_scheduler_spec.rb b/spec/workers/scheduler/sidekiq_health_scheduler_spec.rb
deleted file mode 100644
index 899b09fee3..0000000000
--- a/spec/workers/scheduler/sidekiq_health_scheduler_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-describe Scheduler::SidekiqHealthScheduler do
- let(:worker) { described_class.new }
-
- describe 'perform' do
- it 'runs without error' do
- expect { worker.perform }.to_not raise_error
- end
- end
-end
From 5ba8141df9f2768037d75baf9eac7e03e0cbfe0e Mon Sep 17 00:00:00 2001
From: KMY
Date: Wed, 10 Jan 2024 18:20:12 +0900
Subject: [PATCH 044/203] =?UTF-8?q?Remove:=20#372=20=E5=89=8A=E9=99=A4?=
=?UTF-8?q?=E4=BA=88=E5=AE=9A=E3=81=AE=E3=83=89=E3=83=A1=E3=82=A4=E3=83=B3?=
=?UTF-8?q?=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E9=A0=85=E7=9B=AE=E3=82=92?=
=?UTF-8?q?=E3=81=84=E3=81=A3=E3=81=9F=E3=82=93=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../admin/domain_blocks_controller.rb | 6 ++---
.../api/v1/admin/domain_blocks_controller.rb | 4 ++--
app/lib/account_statuses_filter.rb | 24 +++++++++++++++----
app/lib/status_reach_finder.rb | 3 ---
app/models/domain_block.rb | 3 ---
app/policies/status_policy.rb | 5 +---
.../rest/admin/domain_block_serializer.rb | 4 ++--
app/views/admin/domain_blocks/edit.html.haml | 9 -------
app/views/admin/domain_blocks/new.html.haml | 9 -------
.../_domain_block.html.haml | 3 ---
config/locales/en.yml | 6 -----
config/locales/ja.yml | 9 -------
.../api/v1/admin/domain_blocks_spec.rb | 6 -----
13 files changed, 27 insertions(+), 64 deletions(-)
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index ed8f425de9..f4039ca8f3 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -88,17 +88,17 @@ module Admin
end
def update_params
- params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
+ params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
:reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
def resource_params
- params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
+ params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_media, :reject_send_sensitive, :reject_hashtag,
:reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
def form_domain_block_batch_params
- params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media,
+ params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_media,
:reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden])
end
diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb
index c1a45145cb..130e58dc4d 100644
--- a/app/controllers/api/v1/admin/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb
@@ -69,7 +69,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end
def domain_block_params
- params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
+ params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
:reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden)
end
@@ -102,7 +102,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end
def resource_params
- params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
+ params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow,
:reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden)
end
end
diff --git a/app/lib/account_statuses_filter.rb b/app/lib/account_statuses_filter.rb
index 9a4a2b5d6e..e470a2ff11 100644
--- a/app/lib/account_statuses_filter.rb
+++ b/app/lib/account_statuses_filter.rb
@@ -29,9 +29,8 @@ class AccountStatusesFilter
available_searchabilities = [:public, :unlisted, :private, :direct, :limited, nil]
available_visibilities = [:public, :public_unlisted, :login, :unlisted, :private, :direct, :limited]
- available_searchabilities = [:public] if domain_block&.reject_send_not_public_searchability
- available_visibilities -= [:public_unlisted] if domain_block&.reject_send_public_unlisted || (domain_block&.detect_invalid_subscription && @account.user&.setting_reject_public_unlisted_subscription)
- available_visibilities -= [:unlisted] if domain_block&.detect_invalid_subscription && @account.user&.setting_reject_unlisted_subscription
+ available_visibilities -= [:public_unlisted] if (domain_block&.detect_invalid_subscription || misskey_software?) && @account.user&.setting_reject_public_unlisted_subscription
+ available_visibilities -= [:unlisted] if (domain_block&.detect_invalid_subscription || misskey_software?) && @account.user&.setting_reject_unlisted_subscription
available_visibilities -= [:login] if current_account.nil?
scope.merge!(scope.where(spoiler_text: ['', nil])) if domain_block&.reject_send_sensitive
@@ -44,7 +43,7 @@ class AccountStatusesFilter
private
def initial_scope
- if (suspended? || (domain_block&.reject_send_dissubscribable && @account.dissubscribable)) || domain_block&.reject_send_media || blocked?
+ if suspended? || domain_block&.reject_send_media || blocked?
Status.none
elsif anonymous?
account.statuses.where(visibility: %i(public unlisted public_unlisted))
@@ -156,6 +155,21 @@ class AccountStatusesFilter
end
def domain_block
- @domain_block = DomainBlock.find_by(domain: @account&.domain)
+ return nil if @account.nil? || @account.local?
+
+ @domain_block = DomainBlock.find_by(domain: @account.domain)
+ end
+
+ def misskey_software?
+ return false if @account.nil? || @account.local?
+ return false if instance_info.nil?
+
+ %w(misskey cherrypick).include?(instance_info.software)
+ end
+
+ def instance_info
+ return @instance_info if defined?(@instance_info)
+
+ @instance_info = InstanceInfo.find_by(domain: @account.domain)
end
end
diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb
index c7069c7dac..e67f3a9f33 100644
--- a/app/lib/status_reach_finder.rb
+++ b/app/lib/status_reach_finder.rb
@@ -143,9 +143,6 @@ class StatusReachFinder
[]
else
blocks = DomainBlock.where(domain: nil)
- blocks = blocks.or(DomainBlock.where(reject_send_not_public_searchability: true)) if status.compute_searchability != 'public'
- blocks = blocks.or(DomainBlock.where(reject_send_public_unlisted: true)) if status.public_unlisted_visibility?
- blocks = blocks.or(DomainBlock.where(reject_send_dissubscribable: true)) if status.account.dissubscribable
blocks = blocks.or(DomainBlock.where(reject_send_media: true)) if status.with_media?
blocks = blocks.or(DomainBlock.where(reject_send_sensitive: true)) if (status.with_media? && status.sensitive) || status.spoiler_text?
blocks.pluck(:domain).uniq
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index 37703fec99..9d78db0fc1 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -59,9 +59,6 @@ class DomainBlock < ApplicationRecord
reject_favourite? ? :reject_favourite : nil,
reject_reply? ? :reject_reply : nil,
reject_reply_exclude_followers? ? :reject_reply_exclude_followers : nil,
- reject_send_not_public_searchability? ? :reject_send_not_public_searchability : nil,
- reject_send_public_unlisted? ? :reject_send_public_unlisted : nil,
- reject_send_dissubscribable? ? :reject_send_dissubscribable : nil,
reject_send_media? ? :reject_send_media : nil,
reject_send_sensitive? ? :reject_send_sensitive : nil,
reject_hashtag? ? :reject_hashtag : nil,
diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb
index 335abe9e92..585f54146b 100644
--- a/app/policies/status_policy.rb
+++ b/app/policies/status_policy.rb
@@ -121,10 +121,7 @@ class StatusPolicy < ApplicationPolicy
(@domain_block.detect_invalid_subscription && status.public_unlisted_visibility? && status.account.user&.setting_reject_public_unlisted_subscription) ||
(@domain_block.detect_invalid_subscription && status.public_visibility? && status.account.user&.setting_reject_unlisted_subscription)
else
- (@domain_block.reject_send_not_public_searchability && status.compute_searchability != 'public') ||
- (@domain_block.reject_send_public_unlisted && status.public_unlisted_visibility?) ||
- (@domain_block.reject_send_dissubscribable && status.account.dissubscribable) ||
- (@domain_block.detect_invalid_subscription && status.public_unlisted_visibility? && status.account.user&.setting_reject_public_unlisted_subscription) ||
+ (@domain_block.detect_invalid_subscription && status.public_unlisted_visibility? && status.account.user&.setting_reject_public_unlisted_subscription) ||
(@domain_block.detect_invalid_subscription && status.public_visibility? && status.account.user&.setting_reject_unlisted_subscription) ||
(@domain_block.reject_send_media && status.with_media?) ||
(@domain_block.reject_send_sensitive && ((status.with_media? && status.sensitive) || status.spoiler_text?))
diff --git a/app/serializers/rest/admin/domain_block_serializer.rb b/app/serializers/rest/admin/domain_block_serializer.rb
index e7cdc40ad1..a4776ba4ce 100644
--- a/app/serializers/rest/admin/domain_block_serializer.rb
+++ b/app/serializers/rest/admin/domain_block_serializer.rb
@@ -3,8 +3,8 @@
class REST::Admin::DomainBlockSerializer < ActiveModel::Serializer
attributes :id, :domain, :created_at, :severity,
:reject_media, :reject_favourite, :reject_reply, :reject_reports,
- :reject_send_not_public_searchability, :reject_reply_exclude_followers,
- :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive,
+ :reject_reply_exclude_followers,
+ :reject_send_media, :reject_send_sensitive,
:reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription,
:private_comment, :public_comment, :obfuscate
diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml
index 4a59178c08..9f189d4ee6 100644
--- a/app/views/admin/domain_blocks/edit.html.haml
+++ b/app/views/admin/domain_blocks/edit.html.haml
@@ -23,15 +23,6 @@
.fields-group
= f.input :reject_reply_exclude_followers, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply_exclude_followers'), hint: I18n.t('admin.domain_blocks.reject_reply_exclude_followers_hint')
- .fields-group
- = f.input :reject_send_not_public_searchability, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_not_public_searchability'), hint: I18n.t('admin.domain_blocks.reject_send_not_public_searchability_hint')
-
- .fields-group
- = f.input :reject_send_dissubscribable, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_dissubscribable'), hint: I18n.t('admin.domain_blocks.reject_send_dissubscribable_hint')
-
- .fields-group
- = f.input :reject_send_public_unlisted, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_public_unlisted'), hint: I18n.t('admin.domain_blocks.reject_send_public_unlisted_hint')
-
.fields-group
= f.input :reject_send_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_media'), hint: I18n.t('admin.domain_blocks.reject_send_media_hint')
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index bb64b515fd..e6649485fa 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -23,15 +23,6 @@
.fields-group
= f.input :reject_reply_exclude_followers, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply_exclude_followers'), hint: I18n.t('admin.domain_blocks.reject_reply_exclude_followers_hint')
- .fields-group
- = f.input :reject_send_not_public_searchability, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_not_public_searchability'), hint: I18n.t('admin.domain_blocks.reject_send_not_public_searchability_hint')
-
- .fields-group
- = f.input :reject_send_dissubscribable, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_dissubscribable'), hint: I18n.t('admin.domain_blocks.reject_send_dissubscribable_hint')
-
- .fields-group
- = f.input :reject_send_public_unlisted, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_public_unlisted'), hint: I18n.t('admin.domain_blocks.reject_send_public_unlisted_hint')
-
.fields-group
= f.input :reject_send_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_media'), hint: I18n.t('admin.domain_blocks.reject_send_media_hint')
diff --git a/app/views/admin/export_domain_blocks/_domain_block.html.haml b/app/views/admin/export_domain_blocks/_domain_block.html.haml
index 6b2f0227f5..360a2e7384 100644
--- a/app/views/admin/export_domain_blocks/_domain_block.html.haml
+++ b/app/views/admin/export_domain_blocks/_domain_block.html.haml
@@ -13,9 +13,6 @@
= f.hidden_field :reject_favourite
= f.hidden_field :reject_reply
= f.hidden_field :reject_reply_exclude_followers
- = f.hidden_field :reject_send_not_public_searchability
- = f.hidden_field :reject_send_public_unlisted
- = f.hidden_field :reject_send_dissubscribable
= f.hidden_field :reject_send_media
= f.hidden_field :reject_send_sensitive
= f.hidden_field :reject_hashtag
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ecb59c9463..8e32d3f273 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -438,14 +438,8 @@ en:
reject_reply_exclude_followers_hint: Reject replies exclude followers in the future
reject_reports: Reject reports
reject_reports_hint: Ignore all reports coming from this domain. Irrelevant for suspensions
- reject_send_dissubscribable: 購読拒否アカウントの投稿を配送しない
- reject_send_dissubscribable_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_media: 画像付き投稿を配送しない
reject_send_media_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
- reject_send_not_public_searchability: 検索許可が「誰でも」でない投稿を配送しない
- reject_send_not_public_searchability_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
- reject_send_public_unlisted: ローカル公開投稿を配送しない
- reject_send_public_unlisted_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_sensitive: センシティブな投稿を配送しない
reject_send_sensitive_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_unlisted_dissubscribable: 購読拒否アカウントの未収載投稿を配送しない
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 89684ce637..9977523292 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -432,14 +432,8 @@ ja:
reject_reply_exclude_followers_hint: 今後のリプライを拒否します。停止とは無関係です
reject_reports: 通報を拒否
reject_reports_hint: このドメインからの通報をすべて無視します。停止とは無関係です
- reject_send_dissubscribable: 購読拒否アカウントの投稿を配送しない
- reject_send_dissubscribable_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_media: 画像付き投稿を配送しない
reject_send_media_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
- reject_send_not_public_searchability: 検索許可が「誰でも」でない投稿を配送しない
- reject_send_not_public_searchability_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
- reject_send_public_unlisted: ローカル公開投稿を配送しない
- reject_send_public_unlisted_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_sensitive: センシティブな投稿を配送しない
reject_send_sensitive_hint: 相手サーバーからのフェッチは防げません。停止とは無関係です
reject_send_unlisted_dissubscribable: 購読拒否アカウントの未収載投稿を配送しない
@@ -518,10 +512,7 @@ ja:
reject_reply: リプライを拒否
reject_reply_exclude_followers: フォロー相手以外からのリプライを拒否
reject_reports: 通報を拒否
- reject_send_dissubscribable: 購読拒否投稿配送なし
reject_send_media: メディア付き投稿配送なし
- reject_send_not_public_searchability: 検索許可全て投稿配送なし
- reject_send_public_unlisted: ローカル公開投稿配送なし
reject_send_sensitive: センシティブ投稿配送なし
reject_send_unlisted_dissubscribable: 購読拒否未収載投稿配送なし
reject_straight_follow: フォローを制限
diff --git a/spec/requests/api/v1/admin/domain_blocks_spec.rb b/spec/requests/api/v1/admin/domain_blocks_spec.rb
index 1432e52623..c5fa7f15fb 100644
--- a/spec/requests/api/v1/admin/domain_blocks_spec.rb
+++ b/spec/requests/api/v1/admin/domain_blocks_spec.rb
@@ -78,10 +78,7 @@ RSpec.describe 'Domain Blocks' do
reject_new_follow: domain_block.reject_new_follow,
reject_reply: domain_block.reject_reply,
reject_reply_exclude_followers: domain_block.reject_reply_exclude_followers,
- reject_send_dissubscribable: domain_block.reject_send_dissubscribable,
reject_send_media: domain_block.reject_send_media,
- reject_send_not_public_searchability: domain_block.reject_send_not_public_searchability,
- reject_send_public_unlisted: domain_block.reject_send_public_unlisted,
reject_send_sensitive: domain_block.reject_send_sensitive,
reject_straight_follow: domain_block.reject_straight_follow,
}
@@ -130,10 +127,7 @@ RSpec.describe 'Domain Blocks' do
reject_new_follow: domain_block.reject_new_follow,
reject_reply: domain_block.reject_reply,
reject_reply_exclude_followers: domain_block.reject_reply_exclude_followers,
- reject_send_dissubscribable: domain_block.reject_send_dissubscribable,
reject_send_media: domain_block.reject_send_media,
- reject_send_not_public_searchability: domain_block.reject_send_not_public_searchability,
- reject_send_public_unlisted: domain_block.reject_send_public_unlisted,
reject_send_sensitive: domain_block.reject_send_sensitive,
reject_straight_follow: domain_block.reject_straight_follow,
}
From 4f6d89f161f581fa260ff9a4dcec775c483ffe83 Mon Sep 17 00:00:00 2001
From: KMY
Date: Wed, 10 Jan 2024 18:58:51 +0900
Subject: [PATCH 045/203] Fix test
---
spec/lib/status_reach_finder_spec.rb | 36 +++-------------------------
1 file changed, 3 insertions(+), 33 deletions(-)
diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb
index 57946d3a70..ee1a9668b9 100644
--- a/spec/lib/status_reach_finder_spec.rb
+++ b/spec/lib/status_reach_finder_spec.rb
@@ -176,45 +176,15 @@ describe StatusReachFinder do
let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, uri: 'https://example.com/', inbox_url: 'https://example.com/inbox') }
let(:tom) { Fabricate(:account, username: 'tom', domain: 'tom.com', protocol: :activitypub, uri: 'https://tom.com/', inbox_url: 'https://tom.com/inbox') }
- context 'when reject_send_not_public_searchability' do
- let(:properties) { { reject_send_not_public_searchability: true } }
- let(:searchability) { :private }
+ context 'when reject_send_sensitive' do
+ let(:properties) { { reject_send_sensitive: true } }
+ let(:spoiler_text) { 'CW' }
it 'does not include the inbox of blocked domain' do
expect(subject.inboxes).to_not include 'https://example.com/inbox'
expect(subject.inboxes).to include 'https://tom.com/inbox'
end
end
-
- context 'when reject_send_public_unlisted' do
- let(:properties) { { reject_send_public_unlisted: true } }
- let(:visibility) { :public_unlisted }
-
- it 'does not include the inbox of blocked domain' do
- expect(subject.inboxes).to_not include 'https://example.com/inbox'
- expect(subject.inboxes).to include 'https://tom.com/inbox'
- end
-
- context 'when reject_send_dissubscribable' do
- let(:properties) { { reject_send_dissubscribable: true } }
- let(:dissubscribable) { true }
-
- it 'does not include the inbox of blocked domain' do
- expect(subject.inboxes).to_not include 'https://example.com/inbox'
- expect(subject.inboxes).to include 'https://tom.com/inbox'
- end
- end
-
- context 'when reject_send_sensitive' do
- let(:properties) { { reject_send_sensitive: true } }
- let(:spoiler_text) { 'CW' }
-
- it 'does not include the inbox of blocked domain' do
- expect(subject.inboxes).to_not include 'https://example.com/inbox'
- expect(subject.inboxes).to include 'https://tom.com/inbox'
- end
- end
- end
end
end
end
From 4a5f0f9259512ef9ec5e3d9c094abbb0bff02241 Mon Sep 17 00:00:00 2001
From: KMY
Date: Thu, 11 Jan 2024 08:32:30 +0900
Subject: [PATCH 046/203] =?UTF-8?q?Remove:=20`reject=5Fsend=5Fmedia`?=
=?UTF-8?q?=E3=82=82=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.yarn/install-state.gz | Bin 0 -> 1883069 bytes
.../admin/domain_blocks_controller.rb | 6 +++---
.../api/v1/admin/domain_blocks_controller.rb | 4 ++--
app/lib/account_statuses_filter.rb | 2 +-
app/lib/status_reach_finder.rb | 1 -
app/models/domain_block.rb | 1 -
app/policies/status_policy.rb | 1 -
.../rest/admin/domain_block_serializer.rb | 2 +-
app/views/admin/domain_blocks/edit.html.haml | 3 ---
app/views/admin/domain_blocks/new.html.haml | 3 ---
.../_domain_block.html.haml | 1 -
config/locales/en.yml | 2 --
config/locales/ja.yml | 3 ---
.../api/v1/admin/domain_blocks_spec.rb | 2 --
14 files changed, 7 insertions(+), 24 deletions(-)
create mode 100644 .yarn/install-state.gz
diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz
new file mode 100644
index 0000000000000000000000000000000000000000..4dc538b2ca2c3235193aa0544c8b1e9cb39e40fc
GIT binary patch
literal 1883069
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