Merge pull request #3017 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to e28b64ac2d
This commit is contained in:
Claire 2025-03-28 20:09:09 +01:00 committed by GitHub
commit 51a8c6c3f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 439 additions and 259 deletions

View file

@ -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": "Асабістае",

View file

@ -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",

View file

@ -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

View file

@ -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 <strong>%{domain}</strong> 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.

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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)