diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index bc669a89c6..ea4bf2543c 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -504,6 +504,7 @@ "navigation_bar.follows_and_followers": "Падпіскі і падпісчыкі", "navigation_bar.lists": "Спісы", "navigation_bar.logout": "Выйсці", + "navigation_bar.moderation": "Мадэрацыя", "navigation_bar.mutes": "Ігнараваныя карыстальнікі", "navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.", "navigation_bar.personal": "Асабістае", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index aff66c97f8..0780af3e45 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -669,7 +669,7 @@ "notifications_permission_banner.title": "Nenechte si nic uniknout", "onboarding.follows.back": "Zpět", "onboarding.follows.done": "Hotovo", - "onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit vyhledat nebo procházet stránku s průzkumem a najít lidi, kteří budou sledovat, nebo to zkuste znovu později.", + "onboarding.follows.empty": "Bohužel, žádné výsledky nelze momentálně zobrazit. Můžete zkusit najít uživatele ke sledování za pomocí vyhledávání nebo na stránce „Objevit“, nebo to zkuste znovu později.", "onboarding.follows.search": "Hledat", "onboarding.follows.title": "Sledujte lidi a začněte", "onboarding.profile.discoverable": "Udělat svůj profil vyhledatelným", diff --git a/app/lib/account_reach_finder.rb b/app/lib/account_reach_finder.rb index 19464024a6..4bf5c229a5 100644 --- a/app/lib/account_reach_finder.rb +++ b/app/lib/account_reach_finder.rb @@ -10,7 +10,7 @@ class AccountReachFinder end def inboxes - (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + relay_inboxes).uniq + (followers_inboxes + reporters_inboxes + recently_mentioned_inboxes + recently_followed_inboxes + recently_requested_inboxes + relay_inboxes).uniq end private @@ -31,13 +31,32 @@ class AccountReachFinder .take(RECENT_LIMIT) end + def recently_followed_inboxes + @account + .following + .where(follows: { created_at: recent_date_cutoff... }) + .inboxes + .take(RECENT_LIMIT) + end + + def recently_requested_inboxes + Account + .where(id: @account.follow_requests.where({ created_at: recent_date_cutoff... }).select(:target_account_id)) + .inboxes + .take(RECENT_LIMIT) + end + def relay_inboxes Relay.enabled.pluck(:inbox_url) end def oldest_status_id Mastodon::Snowflake - .id_at(STATUS_SINCE.ago, with_random: false) + .id_at(recent_date_cutoff, with_random: false) + end + + def recent_date_cutoff + @account.suspended? && @account.suspension_origin_local? ? @account.suspended_at - STATUS_SINCE : STATUS_SINCE.ago end def recent_statuses diff --git a/config/locales/lv.yml b/config/locales/lv.yml index 3dbfb0e2b2..16ecb6e0c7 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -429,7 +429,7 @@ lv: obfuscate: Apslēpt domēna vārdu obfuscate_hint: Daļēji apslēpt domēna nosaukumu sarakstā, ja ir iespējota domēna ierobežojumu saraksta reklamēšana private_comment: Privāts komentārs - private_comment_hint: Atstāj komentāru par šo domēna ierobežojumu moderatoru iekšējai lietošanai. + private_comment_hint: Atstāt piebildi par šo domēna ierobežojumu satura pārraudzītāju iekšējai lietošanai. public_comment: Publisks komentārs public_comment_hint: Piebilde par šo domēna ierobežojumu vispārējai sabiedrībai, ja ir iespējota domēnu ierobežojumu saraksta reklamēšana. reject_media: Noraidīt multivides failus @@ -647,7 +647,7 @@ lv: delete: Dzēst placeholder: Jāapraksta veiktās darbības vai jebkuri citi saistītie atjauninājumi... title: Piezīmes - notes_description_html: Skati un atstāj piezīmes citiem moderatoriem un sev nākotnei + notes_description_html: Apskatīt un atstāt piezīmes citiem satura pārraudzītājiem un sev nākotnei processed_msg: 'Pārskats #%{id} veiksmīgi apstrādāts' quick_actions_description_html: 'Veic ātro darbību vai ritini uz leju, lai skatītu saturu, par kuru ziņots:' remote_user_placeholder: attālais lietotājs no %{instance} @@ -1048,6 +1048,8 @@ lv: title: Tīmekļa āķi webhook: Tīmekļa āķis admin_mailer: + auto_close_registrations: + body: Nesenu satura pārraudzības darbību trūkuma dēļ reģistrācija %{instance} ir automātiski pārslēgta nepieciešamība pēc pašrocīgas izskatīšanas, lai novērstu %{instance} izmantošana kā platformu iespējami sliktiem dalībniekiem. Jebkurā brīdī var ieslēgt atpakaļ atvērtu reģistrēšanos. new_appeal: actions: delete_statuses: lai izdzēstu viņu ierakstus @@ -1166,8 +1168,8 @@ lv: accept: Pieņemt back: Atpakaļ invited_by: 'Tu vari pievienoties %{domain}, pateicoties uzaicinājumam, ko saņēmi no:' - preamble: Tos iestata un ievieš %{domain} moderatori. - preamble_invited: Pirms turpināt, lūdzu, apsver galvenos noteikumus, ko noteikuši %{domain} moderatori. + preamble: Tos iestata un ievieš %{domain} satura pārraudzītāji. + preamble_invited: Pirms turpināt, lūgums apsvērt pamatnoteikumus, kurus norādījuši %{domain} satura pārraudzītāji. title: Daži pamatnoteikumi. title_invited: Tu esi uzaicināts. security: Drošība @@ -1181,7 +1183,7 @@ lv: preamble_html: Jāpiesakās ar saviem %{domain} piekļuves datiem. Ja konts tiek mitināts citā serverī, šeit nevarēs pieteikties. title: Pieteikties %{domain} sign_up: - manual_review: Reģistrācijas domēnā %{domain} manuāli pārbauda mūsu moderatori. Lai palīdzētu mums apstrādāt tavu reģistrāciju, uzraksti mazliet par sevi un to, kāpēc vēlies kontu %{domain}. + manual_review: Reģistrāciju %{domain} pašrocīgi izskata mūsu satura pārraudzītāji. Lai palīdzētu mums apstrādāt Tavu reģistrāciju, uzraksti mazliet par sevi un to, kāpēc vēlies kontu %{domain}! title: Atļauj tevi iestatīt %{domain}. status: account_status: Konta statuss @@ -1730,6 +1732,7 @@ lv: user_domain_block: Jūs bloķējāt %{target_name} lost_followers: Zaudētie sekotāji lost_follows: Zaudētie sekojumi + preamble: Tu vari zaudēt sekojamos un sekotājus, kad liedz domēnu vai kad satura pārraudzītāji izlemj apturēt attālu serveri. Kad t as notiek, būs iespējams lejupielādēt sarakstus ar pārtrauktajām saiknēm, kurus tad var izpētīt un, iespējams, ievietot citā serverī. type: Notikums statuses: attached: @@ -1878,9 +1881,9 @@ lv: spam: Spams violation: Saturs pārkāpj šādas kopienas pamatnostādnes explanation: - delete_statuses: Tika konstatēts, ka dažas no tavām ziņām pārkāpj vienu vai vairākas kopienas vadlīnijas, un rezultātā %{instance} moderatori tās noņēma. + delete_statuses: Tika noteikts, ka daži no Taviem ierakstiem pārkāpj vienu vai vairākas kopienas vadlīnijas, tādējādi tos noņēma %{instance} satura pārraudzītāji. disable: Tu vairs nevari izmantot savu kontu, taču tavs profils un citi dati paliek neskarti. Tu vari pieprasīt savu datu dublējumu, mainīt konta iestatījumus vai dzēst kontu. - mark_statuses_as_sensitive: "%{instance} satura pārraudzītāji dažus no Taviem ierakstiem ir atzīmējuši kā jūtīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots priekšskatījums. Tu pats vari atzīmēt informācijas nesēju kā jūtīgu, kad nākotnē tādu ievietosi." + mark_statuses_as_sensitive: "%{instance} satura pārraudzītāji dažus no Taviem ierakstiem ir atzīmējuši kā jūtīgus. Tas nozīmē, ka cilvēkiem būs jāpiesit ierakstos esošajiem informāijas nesējiem, pirms tiek attēlots to priekšskatījums. Tu pats vari atzīmēt informācijas nesēju kā jūtīgu, kad nākotnē tādu ievietosi." sensitive: Turpmāk visi augšupielādētās informācijas nesēju datnes tiks atzīmētas kā jūtīgas un paslēptas aiz klikšķināma brīdinājuma. silence: Tu joprojām vari izmantot savu kontu, taču tikai tie cilvēki, kuri jau tev seko, redzēs tavas ziņas šajā serverī, un tev var tikt liegtas dažādas atklāšanas funkcijas. Tomēr citi joprojām var tev manuāli sekot. suspend: Tu vairs nevari izmantot savu kontu, un tavs profils un citi dati vairs nav pieejami. Tu joprojām vari pieteikties, lai pieprasītu savu datu dublēšanu, līdz dati tiks pilnībā noņemti aptuveni 30 dienu laikā, taču mēs saglabāsim dažus pamata datus, lai neļautu tev izvairīties no apturēšanas. diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml index 905ff30675..f917183f55 100644 --- a/config/locales/simple_form.fr-CA.yml +++ b/config/locales/simple_form.fr-CA.yml @@ -75,6 +75,7 @@ fr-CA: filters: action: Choisir l'action à effectuer quand un message correspond au filtre actions: + blur: Cacher les médias derrière un avertissement, sans cacher le texte hide: Cacher complètement le contenu filtré, faire comme s'il n'existait pas warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre form_admin_settings: @@ -260,6 +261,7 @@ fr-CA: name: Mot-clic filters: actions: + blur: Masquer les médias derrière un avertissement hide: Cacher complètement warn: Cacher derrière un avertissement form_admin_settings: diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 3802a7f32f..a8d6bb86e0 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -75,6 +75,7 @@ fr: filters: action: Choisir l'action à effectuer quand un message correspond au filtre actions: + blur: Cacher les médias derrière un avertissement, sans cacher le texte hide: Cacher complètement le contenu filtré, faire comme s'il n'existait pas warn: Cacher le contenu filtré derrière un avertissement mentionnant le nom du filtre form_admin_settings: @@ -260,6 +261,7 @@ fr: name: Hashtag filters: actions: + blur: Masquer les médias derrière un avertissement hide: Cacher complètement warn: Cacher derrière un avertissement form_admin_settings: diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index ccf3ebcd4f..3773123f17 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -75,6 +75,7 @@ gl: filters: action: Elixe a acción a realizar cando algunha publicación coincida co filtro actions: + blur: Ocultar multimedia detrás dun aviso, sen ocultar o texto que se inclúa hide: Agochar todo o contido filtrado, facer coma se non existise warn: Agochar o contido filtrado tras un aviso que conteña o nome do filtro form_admin_settings: @@ -260,6 +261,7 @@ gl: name: Cancelo filters: actions: + blur: Ocultar multimedia cun aviso hide: Agochar completamente warn: Agochar tras un aviso form_admin_settings: diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index 19e517340d..0c362b0a30 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -26,7 +26,7 @@ lv: types: disable: Neļauj lietotājam izmantot savu kontu, bet neizdzēs vai neslēp tā saturu. none: Izmanto šo, lai nosūtītu lietotājam brīdinājumu, neradot nekādas citas darbības. - sensitive: Piespiest visus šī lietotāja multivides pielikumus atzīmēt kā sensitīvus. + sensitive: Visus šī lietotāja informācijas nesēju pielikumus uzspiesti atzīmēt kā jūtīgus. silence: Neļaut lietotājam veikt ierakstus ar publisku redzamību, paslēpt viņa ierakstus un paziņojumus no cilvēkiem, kas tam neseko. Tiek aizvērti visi ziņojumi par šo kontu. suspend: Novērs jebkādu mijiedarbību no šī konta vai uz to un dzēs tā saturu. Atgriežams 30 dienu laikā. Tiek aizvērti visi šī konta pārskati. warning_preset_id: Neobligāts. Tu joprojām vari pievienot pielāgotu tekstu sākotnējās iestatīšanas beigās @@ -56,8 +56,8 @@ lv: scopes: Kuriem API lietotnei būs ļauts piekļūt. Ja atlasa augstākā līmeņa tvērumu, nav nepieciešamas atlasīt atsevišķus. setting_aggregate_reblogs: Nerādīt jaunus izcēlumus ziņām, kas nesen tika palielinātas (ietekmē tikai nesen saņemtos palielinājumus) setting_always_send_emails: Parasti e-pasta paziņojumi netiek sūtīti, kad aktīvi izmantojat Mastodon - setting_default_sensitive: Sensitīva multivide pēc noklusējuma ir paslēpti, un tos var atklāt, noklikšķinot - setting_display_media_default: Paslēpt multividi, kas atzīmēta kā sensitīva + setting_default_sensitive: Pēc noklusējuma jūtīgi informācijas nesēji ir paslēpti, un tos var atklāt ar klikšķi + setting_display_media_default: Paslēpt informācijas nesējus, kas atzīmēti kā jūtīgi setting_display_media_hide_all: Vienmēr slēpt multividi setting_display_media_show_all: Vienmēr rādīt multividi setting_system_scrollbars_ui: Attiecas tikai uz darbvirsmas pārlūkiem, kuru pamatā ir Safari vai Chrome @@ -179,7 +179,7 @@ lv: types: disable: Iesaldēt none: Nosūtīt brīdinājumu - sensitive: Sensitīvs + sensitive: Jūtīgs silence: Ierobežot suspend: Apturēt warning_preset_id: Lietot iepriekš iestatītus brīdinājumus @@ -223,7 +223,7 @@ lv: setting_boost_modal: Rādīt apstiprinājuma dialogu pirms izcelšanas setting_default_language: Publicēšanas valoda setting_default_privacy: Publicēšanas privātums - setting_default_sensitive: Atļaut atzīmēt multividi kā sensitīvu + setting_default_sensitive: Vienmēr atzīmēt informācijas nesējus kā jūtīgus setting_delete_modal: Parādīt apstiprinājuma dialogu pirms ziņas dzēšanas setting_disable_hover_cards: Atspējot profila priekšskatījumu pēc kursora novietošanas setting_disable_swiping: Atspējot vilkšanas kustības diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 48817a60d5..5549ef40d6 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -75,6 +75,7 @@ pt-PT: filters: action: Escolha qual a ação a executar quando uma publicação corresponde ao filtro actions: + blur: Esconder multimédia com um aviso à frente, sem esconder o texto hide: Ocultar completamente o conteúdo filtrado, comportando-se como se não existisse warn: Ocultar o conteúdo filtrado por trás de um aviso mencionando o título do filtro form_admin_settings: @@ -260,6 +261,7 @@ pt-PT: name: Etiqueta filters: actions: + blur: Esconder multimédia com um aviso hide: Ocultar por completo warn: Ocultar com um aviso form_admin_settings: diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb deleted file mode 100644 index feca543cb7..0000000000 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ActivityPub::InboxesController do - let(:remote_account) { nil } - - before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - end - - describe 'POST #create' do - context 'with signature' do - let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } - - before do - post :create, body: '{}' - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - - context 'with a specific account' do - subject(:response) { post :create, params: { account_username: account.username }, body: '{}' } - - let(:account) { Fabricate(:account) } - - context 'when account is permanently suspended' do - before do - account.suspend! - account.deletion_request.destroy - end - - it 'returns http gone' do - expect(response).to have_http_status(410) - end - end - - context 'when account is temporarily suspended' do - before do - account.suspend! - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - end - end - end - - context 'with Collection-Synchronization header' do - let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } - let(:synchronization_collection) { remote_account.followers_url } - let(:synchronization_url) { 'https://example.com/followers-for-domain' } - let(:synchronization_hash) { 'somehash' } - let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } - - before do - allow(ActivityPub::FollowersSynchronizationWorker).to receive(:perform_async).and_return(nil) - allow(remote_account).to receive(:local_followers_hash).and_return('somehash') - - request.headers['Collection-Synchronization'] = synchronization_header - post :create, body: '{}' - end - - context 'with mismatching target collection' do - let(:synchronization_collection) { 'https://example.com/followers2' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching domain in partial collection attribute' do - let(:synchronization_url) { 'https://example.org/followers' } - - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with matching digest' do - it 'does not start a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to_not have_received(:perform_async) - end - end - - context 'with mismatching digest' do - let(:synchronization_hash) { 'wronghash' } - - it 'starts a synchronization job' do - expect(ActivityPub::FollowersSynchronizationWorker).to have_received(:perform_async) - end - end - - it 'returns http accepted' do - expect(response).to have_http_status(202) - end - end - - context 'without signature' do - before do - post :create, body: '{}' - end - - it 'returns http not authorized' do - expect(response).to have_http_status(401) - end - end - end -end diff --git a/spec/lib/account_reach_finder_spec.rb b/spec/lib/account_reach_finder_spec.rb index 0c1d92b2da..ed16c07c22 100644 --- a/spec/lib/account_reach_finder_spec.rb +++ b/spec/lib/account_reach_finder_spec.rb @@ -13,13 +13,28 @@ RSpec.describe AccountReachFinder do let(:ap_mentioned_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-3', domain: 'example.com') } let(:ap_mentioned_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.org/inbox-4', domain: 'example.org') } + let(:ap_followed_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-5', domain: 'example.com') } + let(:ap_followed_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-6', domain: 'example.org') } + + let(:ap_requested_example_com) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-7', domain: 'example.com') } + let(:ap_requested_example_org) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/inbox-8', domain: 'example.org') } + let(:unrelated_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/unrelated-inbox', domain: 'example.com') } + let(:old_followed_account) { Fabricate(:account, protocol: :activitypub, inbox_url: 'https://example.com/old-followed-inbox', domain: 'example.com') } before do + travel_to(2.months.ago) { account.follow!(old_followed_account) } + ap_follower_example_com.follow!(account) ap_follower_example_org.follow!(account) ap_follower_with_shared.follow!(account) + account.follow!(ap_followed_example_com) + account.follow!(ap_followed_example_org) + + account.request_follow!(ap_requested_example_com) + account.request_follow!(ap_requested_example_org) + Fabricate(:status, account: account).tap do |status| status.mentions << Mention.new(account: ap_follower_example_com) status.mentions << Mention.new(account: ap_mentioned_with_shared) @@ -44,7 +59,10 @@ RSpec.describe AccountReachFinder do expect(subject) .to include(*follower_inbox_urls) .and include(*mentioned_account_inbox_urls) + .and include(*recently_followed_inbox_urls) + .and include(*recently_requested_inbox_urls) .and not_include(unrelated_account.preferred_inbox_url) + .and not_include(old_followed_account.preferred_inbox_url) end def follower_inbox_urls @@ -56,5 +74,15 @@ RSpec.describe AccountReachFinder do [ap_mentioned_with_shared, ap_mentioned_example_com, ap_mentioned_example_org] .map(&:preferred_inbox_url) end + + def recently_followed_inbox_urls + [ap_followed_example_com, ap_followed_example_org] + .map(&:preferred_inbox_url) + end + + def recently_requested_inbox_urls + [ap_requested_example_com, ap_requested_example_org] + .map(&:preferred_inbox_url) + end end end diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/requests/activitypub/collections_spec.rb similarity index 59% rename from spec/controllers/activitypub/collections_controller_spec.rb rename to spec/requests/activitypub/collections_spec.rb index 408e0dd2f6..d2761f98ea 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/requests/activitypub/collections_spec.rb @@ -2,22 +2,19 @@ require 'rails_helper' -RSpec.describe ActivityPub::CollectionsController do +RSpec.describe 'ActivityPub Collections' do let!(:account) { Fabricate(:account) } let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) } let(:remote_account) { nil } before do - allow(controller).to receive(:signed_request_actor).and_return(remote_account) - - Fabricate(:status_pin, account: account) - Fabricate(:status_pin, account: account) + Fabricate.times(2, :status_pin, account: account) Fabricate(:status_pin, account: account, status: private_pinned) Fabricate(:status, account: account, visibility: :private) end describe 'GET #show' do - subject(:response) { get :show, params: { id: id, account_username: account.username } } + subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } context 'when id is "featured"' do let(:id) { 'featured' } @@ -26,10 +23,13 @@ RSpec.describe ActivityPub::CollectionsController do let(:remote_account) { nil } it 'returns http success and correct media type and correct items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -45,17 +45,21 @@ RSpec.describe ActivityPub::CollectionsController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end @@ -65,11 +69,14 @@ RSpec.describe ActivityPub::CollectionsController do context 'when getting a featured resource' do it 'returns http success and correct media type and expected items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -80,39 +87,45 @@ RSpec.describe ActivityPub::CollectionsController do end context 'with authorized fetch mode' do - before do - allow(controller).to receive(:authorized_fetch_mode?).and_return(true) - end + before { Setting.authorized_fetch = true } context 'when signed request account is blocked' do - before do - account.block!(remote_account) - end + before { account.block!(remote_account) } it 'returns http success and correct media type and cache headers and empty items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to include 'private' + subject - expect(response.parsed_body[:orderedItems]) - .to be_an(Array) - .and be_empty + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq('application/activity+json') + expect(response.headers['Cache-Control']) + .to include('private') + + expect(response.parsed_body) + .to include( + orderedItems: be_an(Array).and(be_empty) + ) end end context 'when signed request account is domain blocked' do - before do - account.block_domain!(remote_account.domain) - end + before { account.block_domain!(remote_account.domain) } it 'returns http success and correct media type and cache headers and empty items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to include 'private' + subject - expect(response.parsed_body[:orderedItems]) - .to be_an(Array) - .and be_empty + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq('application/activity+json') + expect(response.headers['Cache-Control']) + .to include('private') + + expect(response.parsed_body) + .to include( + orderedItems: be_an(Array).and(be_empty) + ) end end end @@ -123,7 +136,10 @@ RSpec.describe ActivityPub::CollectionsController do let(:id) { 'hoge' } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end end diff --git a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb b/spec/requests/activitypub/followers_synchronizations_spec.rb similarity index 68% rename from spec/controllers/activitypub/followers_synchronizations_controller_spec.rb rename to spec/requests/activitypub/followers_synchronizations_spec.rb index cbd982f18f..97b8a7908e 100644 --- a/spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +++ b/spec/requests/activitypub/followers_synchronizations_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::FollowersSynchronizationsController do +RSpec.describe 'ActivityPub Follower Synchronizations' do let!(:account) { Fabricate(:account) } let!(:follower_example_com_user_a) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/a') } let!(:follower_example_com_user_b) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/b') } @@ -14,32 +14,34 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do follower_example_com_user_b.follow!(account) follower_foo_com_user_a.follow!(account) follower_example_com_instance_actor.follow!(account) - - allow(controller).to receive(:signed_request_actor).and_return(remote_account) end describe 'GET #show' do context 'without signature' do - let(:remote_account) { nil } - - before do - get :show, params: { account_username: account.username } - end + subject { get account_followers_synchronization_path(account_username: account.username) } it 'returns http not authorized' do - expect(response).to have_http_status(401) + subject + + expect(response) + .to have_http_status(401) end end context 'with signature from example.com' do - subject(:response) { get :show, params: { account_username: account.username } } + subject { get account_followers_synchronization_path(account_username: account.username), headers: nil, sign_with: remote_account } let(:remote_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/instance') } it 'returns http success and cache control and activity json types and correct items' do - expect(response).to have_http_status(200) - expect(response.headers['Cache-Control']).to eq 'max-age=0, private' - expect(response.media_type).to eq 'application/activity+json' + subject + + expect(response) + .to have_http_status(200) + expect(response.headers['Cache-Control']) + .to eq 'max-age=0, private' + expect(response.media_type) + .to eq 'application/activity+json' expect(response.parsed_body[:orderedItems]) .to be_an(Array) @@ -57,17 +59,21 @@ RSpec.describe ActivityPub::FollowersSynchronizationsController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb new file mode 100644 index 0000000000..b21881b10f --- /dev/null +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivityPub Inboxes' do + let(:remote_account) { nil } + + describe 'POST #create' do + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } + + context 'without a named account' do + subject { post inbox_path, params: {}.to_json, sign_with: remote_account } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + + context 'with a specific account' do + subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account } + + let(:account) { Fabricate(:account) } + + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + end + end + end + + context 'with Collection-Synchronization header' do + subject { post inbox_path, params: {}.to_json, headers: { 'Collection-Synchronization' => synchronization_header }, sign_with: remote_account } + + let(:remote_account) { Fabricate(:account, followers_url: 'https://example.com/followers', domain: 'example.com', uri: 'https://example.com/actor', protocol: :activitypub) } + let(:synchronization_collection) { remote_account.followers_url } + let(:synchronization_url) { 'https://example.com/followers-for-domain' } + let(:synchronization_hash) { 'somehash' } + let(:synchronization_header) { "collectionId=\"#{synchronization_collection}\", digest=\"#{synchronization_hash}\", url=\"#{synchronization_url}\"" } + + before do + stub_follow_sync_worker + stub_followers_hash + end + + context 'with mismatching target collection' do + let(:synchronization_collection) { 'https://example.com/followers2' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching domain in partial collection attribute' do + let(:synchronization_url) { 'https://example.org/followers' } + + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with matching digest' do + it 'does not start a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to_not have_received(:perform_async) + end + end + + context 'with mismatching digest' do + let(:synchronization_hash) { 'wronghash' } + + it 'starts a synchronization job' do + subject + + expect(response) + .to have_http_status(202) + expect(ActivityPub::FollowersSynchronizationWorker) + .to have_received(:perform_async) + end + end + + it 'returns http accepted' do + subject + + expect(response) + .to have_http_status(202) + end + + def stub_follow_sync_worker + allow(ActivityPub::FollowersSynchronizationWorker) + .to receive(:perform_async) + .and_return(nil) + end + + def stub_followers_hash + Rails.cache.write("followers_hash:#{remote_account.id}:local", 'somehash') # Populate value to match request + end + end + + context 'without signature' do + subject { post inbox_path, params: {}.to_json } + + it 'returns http not authorized' do + subject + + expect(response) + .to have_http_status(401) + end + end + end +end diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/requests/activitypub/outboxes_spec.rb similarity index 63% rename from spec/controllers/activitypub/outboxes_controller_spec.rb rename to spec/requests/activitypub/outboxes_spec.rb index ca986dcabb..22b2f97c07 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/requests/activitypub/outboxes_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe ActivityPub::OutboxesController do +RSpec.describe 'ActivityPub Outboxes' do let!(:account) { Fabricate(:account) } before do @@ -11,13 +11,11 @@ RSpec.describe ActivityPub::OutboxesController do Fabricate(:status, account: account, visibility: :private) Fabricate(:status, account: account, visibility: :direct) Fabricate(:status, account: account, visibility: :limited) - - allow(controller).to receive(:signed_request_actor).and_return(remote_account) end describe 'GET #show' do context 'without signature' do - subject(:response) { get :show, params: { account_username: account.username, page: page } } + subject { get account_outbox_path(account_username: account.username, page: page) } let(:remote_account) { nil } @@ -25,13 +23,18 @@ RSpec.describe ActivityPub::OutboxesController do let(:page) { nil } it 'returns http success and correct media type and headers and items count' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Vary']).to be_nil - expect(response.parsed_body[:totalItems]).to eq 4 + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Vary']) + .to be_nil + expect(response.parsed_body[:totalItems]) + .to eq 4 end context 'when account is permanently suspended' do @@ -41,17 +44,21 @@ RSpec.describe ActivityPub::OutboxesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end @@ -60,12 +67,16 @@ RSpec.describe ActivityPub::OutboxesController do let(:page) { 'true' } it 'returns http success and correct media type and vary header and items' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Vary']).to include 'Signature' + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Vary']) + .to include 'Signature' expect(response.parsed_body) .to include( @@ -82,35 +93,42 @@ RSpec.describe ActivityPub::OutboxesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do - before do - account.suspend! - end + before { account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end end end context 'with signature' do + subject { get account_outbox_path(account_username: account.username, page: page), headers: nil, sign_with: remote_account } + let(:remote_account) { Fabricate(:account, domain: 'example.com') } let(:page) { 'true' } context 'when signed request account does not follow account' do - before do - get :show, params: { account_username: account.username, page: page } - end - it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -122,15 +140,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account follows account' do - before do - remote_account.follow!(account) - get :show, params: { account_username: account.username, page: page } - end + before { remote_account.follow!(account) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -142,15 +162,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account is blocked' do - before do - account.block!(remote_account) - get :show, params: { account_username: account.username, page: page } - end + before { account.block!(remote_account) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( @@ -160,15 +182,17 @@ RSpec.describe ActivityPub::OutboxesController do end context 'when signed request account is domain blocked' do - before do - account.block_domain!(remote_account.domain) - get :show, params: { account_username: account.username, page: page } - end + before { account.block_domain!(remote_account.domain) } it 'returns http success and correct media type and headers and items' do - expect(response).to have_http_status(200) - expect(response.media_type).to eq 'application/activity+json' - expect(response.headers['Cache-Control']).to eq 'max-age=60, private' + subject + + expect(response) + .to have_http_status(200) + expect(response.media_type) + .to eq 'application/activity+json' + expect(response.headers['Cache-Control']) + .to eq 'private, no-store' expect(response.parsed_body) .to include( diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/requests/activitypub/replies_spec.rb similarity index 78% rename from spec/controllers/activitypub/replies_controller_spec.rb rename to spec/requests/activitypub/replies_spec.rb index d7c2c2d3b0..313cab2a44 100644 --- a/spec/controllers/activitypub/replies_controller_spec.rb +++ b/spec/requests/activitypub/replies_spec.rb @@ -2,9 +2,9 @@ require 'rails_helper' -RSpec.describe ActivityPub::RepliesController do +RSpec.describe 'ActivityPub Replies' do let(:status) { Fabricate(:status, visibility: parent_visibility) } - let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } + let(:remote_account) { Fabricate(:account, domain: 'foobar.com') } let(:remote_reply_id) { 'https://foobar.com/statuses/1234' } let(:remote_querier) { nil } @@ -13,7 +13,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :private } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end @@ -21,7 +24,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :direct } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end end @@ -31,7 +37,10 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :public } it 'returns http not found' do - expect(response).to have_http_status(404) + subject + + expect(response) + .to have_http_status(404) end end @@ -48,19 +57,23 @@ RSpec.describe ActivityPub::RepliesController do end it 'returns http gone' do - expect(response).to have_http_status(410) + subject + + expect(response) + .to have_http_status(410) end end context 'when account is temporarily suspended' do let(:parent_visibility) { :public } - before do - status.account.suspend! - end + before { status.account.suspend! } it 'returns http forbidden' do - expect(response).to have_http_status(403) + subject + + expect(response) + .to have_http_status(403) end end @@ -68,15 +81,20 @@ RSpec.describe ActivityPub::RepliesController do let(:parent_visibility) { :public } it 'returns http success and correct media type' do + subject + expect(response) .to have_http_status(200) .and have_cacheable_headers - expect(response.media_type).to eq 'application/activity+json' + expect(response.media_type) + .to eq 'application/activity+json' end - context 'without only_other_accounts' do + context 'without `only_other_accounts` param' do it "returns items with thread author's replies" do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -91,6 +109,8 @@ RSpec.describe ActivityPub::RepliesController do context 'when there are few self-replies' do it 'points next to replies from other people' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -108,6 +128,8 @@ RSpec.describe ActivityPub::RepliesController do end it 'points next to other self-replies' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -120,31 +142,33 @@ RSpec.describe ActivityPub::RepliesController do end end - context 'with only_other_accounts' do + context 'with `only_other_accounts` param' do let(:only_other_accounts) { 'true' } - it 'returns items with other public or unlisted replies' do + it 'returns items with other public or unlisted replies and correctly inlines replies and uses IDs' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( include(items: be_an(Array).and(have_attributes(size: 3))) ) ) - end - it 'only inlines items that are local and public or unlisted replies' do + # Only inline replies that are local and public, or unlisted expect(inlined_replies) .to all(satisfy { |item| targets_public_collection?(item) }) .and all(satisfy { |item| ActivityPub::TagManager.instance.local_uri?(item[:id]) }) - end - it 'uses ids for remote toots' do + # Use ids for remote replies expect(remote_replies) .to all(satisfy { |item| item.is_a?(String) && !ActivityPub::TagManager.instance.local_uri?(item) }) end context 'when there are few replies' do it 'does not have a next page' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and(not_include(next: be_present)) @@ -158,6 +182,8 @@ RSpec.describe ActivityPub::RepliesController do end it 'points next to other replies' do + subject + expect(response.parsed_body) .to include( first: be_a(Hash).and( @@ -176,10 +202,8 @@ RSpec.describe ActivityPub::RepliesController do before do stub_const 'ActivityPub::RepliesController::DESCENDANTS_LIMIT', 5 - allow(controller).to receive(:signed_request_actor).and_return(remote_querier) - Fabricate(:status, thread: status, visibility: :public) - Fabricate(:status, thread: status, visibility: :public) + Fabricate.times(2, :status, thread: status, visibility: :public) Fabricate(:status, thread: status, visibility: :private) Fabricate(:status, account: status.account, thread: status, visibility: :public) Fabricate(:status, account: status.account, thread: status, visibility: :private) @@ -188,31 +212,29 @@ RSpec.describe ActivityPub::RepliesController do end describe 'GET #index' do - subject(:response) { get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } } - let(:only_other_accounts) { nil } context 'with no signature' do + subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts) } + it_behaves_like 'allowed access' end context 'with signature' do + subject { get account_status_replies_path(account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts), headers: nil, sign_with: remote_querier } + let(:remote_querier) { Fabricate(:account, domain: 'example.com') } it_behaves_like 'allowed access' context 'when signed request account is blocked' do - before do - status.account.block!(remote_querier) - end + before { status.account.block!(remote_querier) } it_behaves_like 'disallowed access' end context 'when signed request account is domain blocked' do - before do - status.account.block_domain!(remote_querier.domain) - end + before { status.account.block_domain!(remote_querier.domain) } it_behaves_like 'disallowed access' end diff --git a/spec/support/signed_request_helpers.rb b/spec/support/signed_request_helpers.rb index 8a52179cae..a4423af748 100644 --- a/spec/support/signed_request_helpers.rb +++ b/spec/support/signed_request_helpers.rb @@ -18,4 +18,24 @@ module SignedRequestHelpers super(path, headers: headers, **args) end + + def post(path, headers: nil, sign_with: nil, **args) + return super(path, headers: headers, **args) if sign_with.nil? + + headers ||= {} + headers['Date'] = Time.now.utc.httpdate + headers['Host'] = Rails.configuration.x.local_domain + headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(args[:params].to_s)}" + + signed_headers = headers.merge('(request-target)' => "post #{path}").slice('(request-target)', 'Host', 'Date', 'Digest') + + key_id = ActivityPub::TagManager.instance.key_uri_for(sign_with) + keypair = sign_with.keypair + signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string)) + + headers['Signature'] = "keyId=\"#{key_id}\",algorithm=\"rsa-sha256\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\"" + + super(path, headers: headers, **args) + end end diff --git a/spec/system/account_notes_spec.rb b/spec/system/account_notes_spec.rb index f8be96be86..c4054f204e 100644 --- a/spec/system/account_notes_spec.rb +++ b/spec/system/account_notes_spec.rb @@ -24,9 +24,6 @@ RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do # The easiest way is to send ctrl+enter ourselves find_field(class: 'account__header__account-note__content').send_keys [:control, :enter] - expect(page) - .to have_css('.account__header__account-note .inline-alert', text: 'SAVED') - expect(page) .to have_css('.account__header__account-note__content', text: note_text)