From 803a8be998b9e00e76e593f7054f08a76fe4e53b Mon Sep 17 00:00:00 2001 From: Shlee Date: Wed, 26 Mar 2025 18:12:15 +1030 Subject: [PATCH 1/5] Add `EXTRA_MEDIA_HOSTS` environment variable to add extra hosts to Content-Security-Policy (#34184) Co-authored-by: Claire --- .env.production.sample | 3 +++ app/lib/content_security_policy.rb | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.env.production.sample b/.env.production.sample index 12ab2b6dcb..4afaf8d756 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -79,6 +79,9 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com +# 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/lib/content_security_policy.rb b/app/lib/content_security_policy.rb index c764d1856d..fc42e2d48b 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].compact + [assets_host, cdn_host_value, paperclip_root_url].concat(extra_media_hosts).compact end def sso_host @@ -31,6 +31,10 @@ class ContentSecurityPolicy private + def extra_media_hosts + ENV.fetch('EXTRA_MEDIA_HOSTS', '').split(/(?:\s*,\s*|\s+)/) + end + def url_from_configured_asset_host Rails.configuration.action_controller.asset_host end From dd6c573cc31f134ebf63d68c89d7a57472989b94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 07:42:25 +0000 Subject: [PATCH 2/5] New Crowdin Translations (automated) (#34276) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/br.json | 30 +++++++++++++++++++++++++ app/javascript/mastodon/locales/et.json | 2 ++ config/locales/activerecord.ko.yml | 2 ++ config/locales/br.yml | 3 +++ config/locales/devise.lv.yml | 20 ++++++++--------- config/locales/simple_form.ko.yml | 6 ++++- 6 files changed, 52 insertions(+), 11 deletions(-) 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/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: From 59e189ad3cd5f2973a71da8dfade68733b2eccd3 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 26 Mar 2025 12:33:59 +0100 Subject: [PATCH 3/5] Add support for paginating partial collections in `SynchronizeFollowersService` (#34277) --- app/models/account.rb | 2 +- .../synchronize_followers_service.rb | 66 +++++++++----- .../synchronize_followers_service_spec.rb | 86 +++++++++++++++++-- 3 files changed, 128 insertions(+), 26 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index 05e833d575..53bf2407e8 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/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/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 From 02db0655716bdd7a7adf3974b2ce169785f224de Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 26 Mar 2025 14:26:24 +0100 Subject: [PATCH 4/5] Use fixed order in flaky spec (#34279) --- spec/requests/api/v2/notifications_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c43508b3e0b05c5e739d726bad53f1eef89e9376 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 26 Mar 2025 15:12:58 +0100 Subject: [PATCH 5/5] Add `registrations.reason_required` attribute to `/api/v2/instance` response (#34280) --- app/serializers/rest/instance_serializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 30da6e2e1a..3d36faf98d 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -106,6 +106,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),