diff --git a/.env.production.sample b/.env.production.sample
index 28aa1e4b9a..a32c6519e1 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -309,6 +309,9 @@ MAX_POLL_OPTION_CHARS=100
# HCAPTCHA_SECRET_KEY=
# HCAPTCHA_SITE_KEY=
+# Optional list of hosts that are allowed to serve media for your instance
+# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com
+
# IP and session retention
# -----------------------
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json
index 9829986174..dde569bf5e 100644
--- a/app/javascript/mastodon/locales/br.json
+++ b/app/javascript/mastodon/locales/br.json
@@ -81,9 +81,14 @@
"alert.rate_limited.title": "Feur bevennet",
"alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
"alert.unexpected.title": "Hopala !",
+ "alt_text_modal.cancel": "Nullañ",
+ "alt_text_modal.change_thumbnail": "Kemmañ ar velvenn",
+ "alt_text_modal.done": "Graet",
"announcement.announcement": "Kemennad",
"annual_report.summary.followers.followers": "heulier",
"annual_report.summary.highlighted_post.possessive": "{name}",
+ "annual_report.summary.most_used_hashtag.none": "Hini ebet",
+ "annual_report.summary.new_posts.new_posts": "toudoù nevez",
"attachments_list.unprocessed": "(ket meret)",
"audio.hide": "Kuzhat ar c'hleved",
"block_modal.show_less": "Diskouez nebeutoc'h",
@@ -109,9 +114,11 @@
"column.blocks": "Implijer·ezed·ien berzet",
"column.bookmarks": "Sinedoù",
"column.community": "Red-amzer lec'hel",
+ "column.create_list": "Krouiñ ul listenn",
"column.direct": "Menegoù prevez",
"column.directory": "Mont a-dreuz ar profiloù",
"column.domain_blocks": "Domani berzet",
+ "column.edit_list": "Kemmañ al listenn",
"column.favourites": "Muiañ-karet",
"column.firehose": "Redoù war-eeun",
"column.follow_requests": "Rekedoù heuliañ",
@@ -162,9 +169,12 @@
"confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?",
"confirmations.delete_list.confirm": "Dilemel",
"confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?",
+ "confirmations.delete_list.title": "Dilemel al listenn?",
"confirmations.discard_edit_media.confirm": "Nac'hañ",
"confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
"confirmations.edit.confirm": "Kemmañ",
+ "confirmations.edit.message": "Kemmañ bremañ a zilamo ar gemennadenn emaoc'h o skrivañ. Sur e oc'h e fell deoc'h kenderc'hel ganti?",
+ "confirmations.follow_to_list.title": "Heuliañ an implijer·ez?",
"confirmations.logout.confirm": "Digevreañ",
"confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
"confirmations.mute.confirm": "Kuzhat",
@@ -266,8 +276,10 @@
"footer.privacy_policy": "Reolennoù prevezded",
"footer.source_code": "Gwelet ar c'hod mammenn",
"footer.status": "Statud",
+ "footer.terms_of_service": "Divizoù implijout hollek",
"generic.saved": "Enrollet",
"getting_started.heading": "Loc'hañ",
+ "hashtag.admin_moderation": "Digeriñ an etrefas evezhiañ evit #{name}",
"hashtag.column_header.tag_mode.all": "ha(g) {additional}",
"hashtag.column_header.tag_mode.any": "pe {additional}",
"hashtag.column_header.tag_mode.none": "hep {additional}",
@@ -337,8 +349,14 @@
"limited_account_hint.action": "Diskouez an aelad memes tra",
"limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.",
"link_preview.author": "Gant {name}",
+ "lists.add_member": "Ouzhpennañ",
+ "lists.add_to_list": "Ouzhpennañ d'al listenn",
+ "lists.create": "Krouiñ",
+ "lists.create_list": "Krouiñ ul listenn",
"lists.delete": "Dilemel al listenn",
+ "lists.done": "Graet",
"lists.edit": "Kemmañ al listenn",
+ "lists.list_name": "Anv al listenn",
"lists.replies_policy.followed": "Pep implijer.ez heuliet",
"lists.replies_policy.list": "Izili ar roll",
"lists.replies_policy.none": "Den ebet",
@@ -373,11 +391,17 @@
"notification.follow": "heuliañ a ra {name} ac'hanoc'h",
"notification.follow.name_and_others": "{name} {count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}} zo o heuliañ ac'hanoc'h",
"notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ",
+ "notification.label.reply": "Respont",
"notification.moderation-warning.learn_more": "Gouzout hiroc'h",
"notification.own_poll": "Echu eo ho sontadeg",
"notification.reblog": "Gant {name} eo bet skignet ho toud",
+ "notification.relationships_severance_event.learn_more": "Gouzout hiroc'h",
"notification.status": "Emañ {name} o paouez toudañ",
"notification.update": "Gant {name} ez eus bet kemmet un toud",
+ "notification_requests.accept": "Asantiñ",
+ "notification_requests.dismiss": "Diverkañ",
+ "notification_requests.edit_selection": "Kemmañ",
+ "notification_requests.exit_selection": "Graet",
"notifications.clear": "Skarzhañ ar c'hemennoù",
"notifications.clear_confirmation": "Ha sur oc'h e fell deoc'h skarzhañ ho holl kemennoù ?",
"notifications.column_settings.admin.report": "Disklêriadurioù nevez :",
@@ -410,6 +434,10 @@
"notifications.permission_denied": "Kemennoù war ar burev n'int ket hegerz rak pedadenn aotren ar merdeer a zo bet nullet araok",
"notifications.permission_denied_alert": "Kemennoù wa ar burev na c'hellont ket bezañ lezelet, rak aotre ar merdeer a zo bet nac'het a-raok",
"notifications.permission_required": "Kemennoù war ar burev n'int ket hegerz abalamour d'an aotre rekis n'eo ket bet roet.",
+ "notifications.policy.accept": "Asantiñ",
+ "notifications.policy.accept_hint": "Diskouez er c’hemennoù",
+ "notifications.policy.drop": "Tremen e-bioù",
+ "notifications.policy.filter": "Silañ",
"notifications.policy.filter_new_accounts_title": "Kontoù nevez",
"notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
"notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
@@ -515,6 +543,7 @@
"search_results.accounts": "Profiloù",
"search_results.all": "Pep tra",
"search_results.hashtags": "Hashtagoù",
+ "search_results.no_results": "Disoc'h ebet.",
"search_results.see_all": "Gwelet pep tra",
"search_results.statuses": "Toudoù",
"server_banner.active_users": "implijerien·ezed oberiant",
@@ -579,6 +608,7 @@
"subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}",
"tabs_bar.home": "Degemer",
"tabs_bar.notifications": "Kemennoù",
+ "terms_of_service.title": "Divizoù implijout",
"time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom",
"time_remaining.hours": "{number, plural, one {# eurvezh} other{# eurvezh}} a chom",
"time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom",
diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json
index 43983e502f..8a7edc1ccb 100644
--- a/app/javascript/mastodon/locales/et.json
+++ b/app/javascript/mastodon/locales/et.json
@@ -562,6 +562,7 @@
"notification.favourite": "{name} märkis su postituse lemmikuks",
"notification.favourite.name_and_others_with_link": "{name} ja {count, plural, one {# veel} other {# teist}} märkis su postituse lemmikuks",
"notification.favourite_pm": "{name} märkis sinu privaatse mainimise lemmikuks",
+ "notification.favourite_pm.name_and_others_with_link": "{name} ja {count, plural, one {# veel} other {# veel}} märkisid su privaatse mainimise lemmikuks",
"notification.follow": "{name} alustas su jälgimist",
"notification.follow.name_and_others": "{name} ja veel {count, plural, one {# kasutaja} other {# kasutajat}} hakkas sind jälgima",
"notification.follow_request": "{name} soovib sind jälgida",
@@ -696,6 +697,7 @@
"poll_button.remove_poll": "Eemalda küsitlus",
"privacy.change": "Muuda postituse nähtavust",
"privacy.direct.long": "Kõik postituses mainitud",
+ "privacy.direct.short": "Privaatne mainimine",
"privacy.private.long": "Ainult jälgijad",
"privacy.private.short": "Jälgijad",
"privacy.public.long": "Nii kasutajad kui mittekasutajad",
diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb
index 2e6c43be8f..1566a40f79 100644
--- a/app/lib/content_security_policy.rb
+++ b/app/lib/content_security_policy.rb
@@ -10,7 +10,7 @@ class ContentSecurityPolicy
end
def media_hosts
- [assets_host, cdn_host_value, paperclip_root_url].concat(extra_data_hosts).compact
+ [assets_host, cdn_host_value, paperclip_root_url].concat(extra_media_hosts).compact
end
def sso_host
@@ -31,8 +31,17 @@ class ContentSecurityPolicy
private
+ # TODO: remove after 4.4.0
def extra_data_hosts
- ENV.fetch('EXTRA_DATA_HOSTS', '').split('|')
+ return [] unless ENV['EXTRA_DATA_HOSTS']
+
+ ENV.fetch('EXTRA_DATA_HOSTS', '').split('|').tap do |hosts|
+ Rails.logger.warn "EXTRA_DATA_HOSTS is deprecated, use EXTRA_MEDIA_HOSTS=#{hosts.join(',')}"
+ end
+ end
+
+ def extra_media_hosts
+ ENV.fetch('EXTRA_MEDIA_HOSTS', '').split(/(?:\s*,\s*|\s+)/).presence || extra_data_hosts
end
def url_from_configured_asset_host
diff --git a/app/models/account.rb b/app/models/account.rb
index ad8f4714eb..dc0a9c9ff2 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -153,7 +153,7 @@ class Account < ApplicationRecord
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
scope :dormant, -> { joins(:account_stat).merge(AccountStat.without_recent_activity) }
- scope :with_username, ->(value) { where arel_table[:username].lower.eq(value.to_s.downcase) }
+ scope :with_username, ->(value) { value.is_a?(Array) ? where(arel_table[:username].lower.in(value.map { |x| x.to_s.downcase })) : where(arel_table[:username].lower.eq(value.to_s.downcase)) }
scope :with_domain, ->(value) { where arel_table[:domain].lower.eq(value&.to_s&.downcase) }
scope :without_memorial, -> { where(memorial: false) }
scope :duplicate_uris, -> { select(:uri, Arel.star.count).group(:uri).having(Arel.star.count.gt(1)) }
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index 87aa430174..1473401972 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -107,6 +107,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
{
enabled: registrations_enabled?,
approval_required: Setting.registrations_mode == 'approved',
+ reason_required: Setting.registrations_mode == 'approved' && Setting.require_invite_text,
message: registrations_enabled? ? nil : registrations_message,
min_age: Setting.min_age.presence,
url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
diff --git a/app/services/activitypub/synchronize_followers_service.rb b/app/services/activitypub/synchronize_followers_service.rb
index f9baca38b9..5b58a025cb 100644
--- a/app/services/activitypub/synchronize_followers_service.rb
+++ b/app/services/activitypub/synchronize_followers_service.rb
@@ -4,32 +4,46 @@ class ActivityPub::SynchronizeFollowersService < BaseService
include JsonLdHelper
include Payloadable
+ MAX_COLLECTION_PAGES = 10
+
def call(account, partial_collection_url)
@account = account
+ @expected_followers_ids = []
- items = collection_items(partial_collection_url)
- return if items.nil?
-
- # There could be unresolved accounts (hence the call to .compact) but this
- # should never happen in practice, since in almost all cases we keep an
- # Account record, and should we not do that, we should have sent a Delete.
- # In any case there is not much we can do if that occurs.
- @expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }
+ return unless process_collection!(partial_collection_url)
remove_unexpected_local_followers!
- handle_unexpected_outgoing_follows!
end
private
+ def process_page!(items)
+ page_expected_followers = extract_local_followers(items)
+ @expected_followers_ids.concat(page_expected_followers.pluck(:id))
+
+ handle_unexpected_outgoing_follows!(page_expected_followers)
+ end
+
+ def extract_local_followers(items)
+ # There could be unresolved accounts (hence the call to .filter_map) but this
+ # should never happen in practice, since in almost all cases we keep an
+ # Account record, and should we not do that, we should have sent a Delete.
+ # In any case there is not much we can do if that occurs.
+
+ # TODO: this will need changes when switching to numeric IDs
+
+ usernames = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username)&.downcase }
+ Account.local.with_username(usernames)
+ end
+
def remove_unexpected_local_followers!
- @account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower|
+ @account.followers.local.where.not(id: @expected_followers_ids).reorder(nil).find_each do |unexpected_follower|
UnfollowService.new.call(unexpected_follower, @account)
end
end
- def handle_unexpected_outgoing_follows!
- @expected_followers.each do |expected_follower|
+ def handle_unexpected_outgoing_follows!(expected_followers)
+ expected_followers.each do |expected_follower|
next if expected_follower.following?(@account)
if expected_follower.requested?(@account)
@@ -50,21 +64,33 @@ class ActivityPub::SynchronizeFollowersService < BaseService
Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
end
- def collection_items(collection_or_uri)
- collection = fetch_collection(collection_or_uri)
- return unless collection.is_a?(Hash)
+ # Only returns true if the whole collection has been processed
+ def process_collection!(collection_uri, max_pages: MAX_COLLECTION_PAGES)
+ collection = fetch_collection(collection_uri)
+ return false unless collection.is_a?(Hash)
collection = fetch_collection(collection['first']) if collection['first'].present?
- return unless collection.is_a?(Hash)
- # Abort if we'd have to paginate through more than one page of followers
- return if collection['next'].present?
+ while collection.is_a?(Hash)
+ process_page!(as_array(collection_page_items(collection)))
+ max_pages -= 1
+
+ return true if collection['next'].blank? # We reached the end of the collection
+ return false if max_pages <= 0 # We reached our pages limit
+
+ collection = fetch_collection(collection['next'])
+ end
+
+ false
+ end
+
+ def collection_page_items(collection)
case collection['type']
when 'Collection', 'CollectionPage'
- as_array(collection['items'])
+ collection['items']
when 'OrderedCollection', 'OrderedCollectionPage'
- as_array(collection['orderedItems'])
+ collection['orderedItems']
end
end
diff --git a/config/locales/activerecord.ko.yml b/config/locales/activerecord.ko.yml
index 5b1542496f..3aa991734b 100644
--- a/config/locales/activerecord.ko.yml
+++ b/config/locales/activerecord.ko.yml
@@ -55,6 +55,8 @@ ko:
too_soon: 너무 이릅니다. %{date} 이후로 지정해야 합니다
user:
attributes:
+ date_of_birth:
+ below_limit: 나이 제한보다 아래입니다
email:
blocked: 허용되지 않은 이메일 제공자입니다
unreachable: 존재하지 않는 것 같습니다
diff --git a/config/locales/br.yml b/config/locales/br.yml
index fbe91fcbd7..f12269eba3 100644
--- a/config/locales/br.yml
+++ b/config/locales/br.yml
@@ -47,6 +47,7 @@ br:
demote: Argilañ
disable: Skornañ
disabled: Skornet
+ display_name: Anv diskouezet
domain: Domani
edit: Kemmañ
email: Postel
@@ -66,6 +67,7 @@ br:
moderation:
active: Oberiant
all: Pep tra
+ disabled: Diweredekaet
pending: War ober
silenced: Bevennet
suspended: Astalet
@@ -98,6 +100,7 @@ br:
action_logs:
action_types:
destroy_status: Dilemel ar c'hannad
+ reset_password_user: Adderaouekaat ar ger-tremen
update_status: Hizivaat ar c'hannad
actions:
destroy_status_html: Dilamet eo bet toud %{target} gant %{name}
diff --git a/config/locales/devise.lv.yml b/config/locales/devise.lv.yml
index c3f4f0df89..e60f8fa62e 100644
--- a/config/locales/devise.lv.yml
+++ b/config/locales/devise.lv.yml
@@ -85,25 +85,25 @@ lv:
title: Drošības atslēgas iespējotas
omniauth_callbacks:
failure: Nevarēja autentificēt tevi no %{kind}, jo "%{reason}".
- success: Veiksmīgi autentificēts no %{kind} konta.
+ success: Sekmīgi autentificēts no %{kind} konta.
passwords:
no_token: Tu nevari piekļūt šai lapai, ja neesi saņēmis paroles atiestatīšanas e-pasta ziņojumu. Ja ienāci no paroles atiestatīšanas e-pasta, lūdzu, pārliecinies, vai izmanto visu norādīto URL.
send_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm savā e-pasta adresē saņemsi paroles atkopes saiti. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
- updated: Tava parole tika veiksmīgi nomainīta. Tagad esi pieteicies.
- updated_not_active: Tava parole ir veiksmīgi nomainīta.
+ updated: Tava parole tika sekmīgi nomainīta. Tagad esi pieteicies.
+ updated_not_active: Tava parole tika sekmīgi nomainīta.
registrations:
- destroyed: Visu labu! Tavs konts ir veiksmīgi atcelts. Mēs ceram tevi drīz atkal redzēt.
- update_needs_confirmation: Tu veiksmīgi atjaunināji savu kontu, taču mums ir jāapliecina Tava jaunā e-pasta adrese. Lūgums pārbaudīt savu e-pastu un sekot apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
- updated: Tavs konts ir veiksmīgi atjaunināts.
+ destroyed: Visu labu! Tavs konts ir sekmīgi atcelts. Mēs ceram Tevi drīz atkal redzēt.
+ update_needs_confirmation: Tu sekmīgi atjaunināji savu kontu, taču mums ir jāapliecina Tava jaunā e-pasta adrese. Lūgums pārbaudīt savu e-pastu un sekot apstiprinājuma saitei, lai apstiprinātu savu jauno e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
+ updated: Tavs konts tika sekmīgi atjaunināts.
sessions:
- already_signed_out: Veiksmīgi izrakstījies.
- signed_in: Veiksmīgi pieteicies.
- signed_out: Veiksmīgi izrakstījies.
+ already_signed_out: Sekmīgi izrakstījies.
+ signed_in: Sekmīgi pierakstījies.
+ signed_out: Sekmīgi izrakstījies.
unlocks:
send_instructions: Pēc dažām minūtēm Tu saņemsi e-pasta ziņojumu ar norādēm, kā atslēgt savu kontu. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
send_paranoid_instructions: Ja Tavs konts pastāv, dažu minūšu laikā saņemsi e-pasta ziņojumu ar norādēm, kā to atslēgt. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
- unlocked: Konts tika veiksmīgi atbloķēts. Lūgums pieteikties, lai turpinātu.
+ unlocked: Konts tika sekmīgi atslēgts. Lūgums pieteikties, lai turpinātu.
errors:
messages:
already_confirmed: jau tika apstiprināts, lūgums mēģināt pieteikties
diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml
index ef3764bed8..bd3fb32c1f 100644
--- a/config/locales/simple_form.ko.yml
+++ b/config/locales/simple_form.ko.yml
@@ -153,7 +153,7 @@ ko:
position: 특정 상황에서 충돌이 발생할 경우 더 높은 역할이 충돌을 해결합니다. 특정 작업은 우선순위가 낮은 역할에 대해서만 수행될 수 있습니다
webhook:
events: 전송할 이벤트를 선택하세요
- template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그냥 냅둬서 기본 JSON을 사용할 수 있습니다.
+ template: 원하는 JSON 페이로드를 변수와 함께 작성하거나, 그대로 두어 기본 JSON을 사용할 수 있습니다.
url: 이벤트가 어디로 전송될 지
labels:
account:
@@ -269,6 +269,7 @@ ko:
favicon: 파비콘
mascot: 사용자 정의 마스코트 (legacy)
media_cache_retention_period: 미디어 캐시 유지 기한
+ min_age: 최소 연령 제한
peers_api_enabled: API에 발견 된 서버들의 목록 발행
profile_directory: 프로필 책자 활성화
registrations_mode: 누가 가입할 수 있는지
@@ -347,6 +348,9 @@ ko:
jurisdiction: 법적 관할권
min_age: 최소 연령
user:
+ date_of_birth_1i: 일
+ date_of_birth_2i: 월
+ date_of_birth_3i: 년
role: 역할
time_zone: 시간대
user_role:
diff --git a/spec/requests/api/v2/notifications_spec.rb b/spec/requests/api/v2/notifications_spec.rb
index a434deaed9..69feb6cb6e 100644
--- a/spec/requests/api/v2/notifications_spec.rb
+++ b/spec/requests/api/v2/notifications_spec.rb
@@ -169,7 +169,7 @@ RSpec.describe 'Notifications' do
end
context 'with min_id param' do
- let(:params) { { min_id: user.account.notifications.reload.first.id - 1 } }
+ let(:params) { { min_id: user.account.notifications.order(id: :asc).first.id - 1 } }
it 'returns a notification group covering all notifications' do
subject
diff --git a/spec/services/activitypub/synchronize_followers_service_spec.rb b/spec/services/activitypub/synchronize_followers_service_spec.rb
index 3399e4fdf0..70f27627e1 100644
--- a/spec/services/activitypub/synchronize_followers_service_spec.rb
+++ b/spec/services/activitypub/synchronize_followers_service_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
let(:bob) { Fabricate(:account, username: 'bob') }
let(:eve) { Fabricate(:account, username: 'eve') }
let(:mallory) { Fabricate(:account, username: 'mallory') }
- let(:collection_uri) { 'http://example.com/partial-followers' }
+ let(:collection_uri) { 'https://example.com/partial-followers' }
let(:items) do
[alice, eve, mallory].map do |account|
@@ -97,7 +97,76 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
it_behaves_like 'synchronizes followers'
end
- context 'when the endpoint is a paginated Collection of actor URIs with a next page' do
+ context 'when the endpoint is a paginated Collection of actor URIs split across multiple pages' do
+ before do
+ stub_request(:get, 'https://example.com/partial-followers')
+ .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ type: 'Collection',
+ id: 'https://example.com/partial-followers',
+ first: 'https://example.com/partial-followers/1',
+ }))
+
+ stub_request(:get, 'https://example.com/partial-followers/1')
+ .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ type: 'CollectionPage',
+ id: 'https://example.com/partial-followers/1',
+ partOf: 'https://example.com/partial-followers',
+ next: 'https://example.com/partial-followers/2',
+ items: [alice, eve].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
+ }))
+
+ stub_request(:get, 'https://example.com/partial-followers/2')
+ .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ type: 'CollectionPage',
+ id: 'https://example.com/partial-followers/2',
+ partOf: 'https://example.com/partial-followers',
+ items: ActivityPub::TagManager.instance.uri_for(mallory),
+ }))
+ end
+
+ it_behaves_like 'synchronizes followers'
+ end
+
+ context 'when the endpoint is a paginated Collection of actor URIs split across, but one page errors out' do
+ before do
+ stub_request(:get, 'https://example.com/partial-followers')
+ .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ type: 'Collection',
+ id: 'https://example.com/partial-followers',
+ first: 'https://example.com/partial-followers/1',
+ }))
+
+ stub_request(:get, 'https://example.com/partial-followers/1')
+ .to_return(status: 200, headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({
+ '@context': 'https://www.w3.org/ns/activitystreams',
+ type: 'CollectionPage',
+ id: 'https://example.com/partial-followers/1',
+ partOf: 'https://example.com/partial-followers',
+ next: 'https://example.com/partial-followers/2',
+ items: [mallory].map { |account| ActivityPub::TagManager.instance.uri_for(account) },
+ }))
+
+ stub_request(:get, 'https://example.com/partial-followers/2')
+ .to_return(status: 404)
+ end
+
+ it 'confirms pending follow request but does not remove extra followers' do
+ previous_follower_ids = actor.followers.pluck(:id)
+
+ subject.call(actor, collection_uri)
+
+ expect(previous_follower_ids - actor.followers.reload.pluck(:id))
+ .to be_empty
+ expect(mallory)
+ .to be_following(actor)
+ end
+ end
+
+ context 'when the endpoint is a paginated Collection of actor URIs with more pages than we allow' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
@@ -113,12 +182,19 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
end
before do
+ stub_const('ActivityPub::SynchronizeFollowersService::MAX_COLLECTION_PAGES', 1)
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end
- it 'does not change followers' do
- expect { subject.call(actor, collection_uri) }
- .to_not(change { actor.followers.reload.reorder(id: :asc).pluck(:id) })
+ it 'confirms pending follow request but does not remove extra followers' do
+ previous_follower_ids = actor.followers.pluck(:id)
+
+ subject.call(actor, collection_uri)
+
+ expect(previous_follower_ids - actor.followers.reload.pluck(:id))
+ .to be_empty
+ expect(mallory)
+ .to be_following(actor)
end
end
end