Compare commits

...

21 Commits

Author SHA1 Message Date
Angus McLeod
79ccaa3bdf
Merge a5979402ce into 94bceb8683 2025-07-11 14:03:35 +00:00
Echo
94bceb8683
Expose enabled features to the frontend (#35348)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-07-11 13:15:22 +00:00
Claire
88b0f3a172
Simplify DatabaseViewRecord.refresh (#35252) 2025-07-11 08:36:05 +00:00
github-actions[bot]
b69b5ba775
New Crowdin Translations (automated) (#35344)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-07-11 08:14:39 +00:00
Angus McLeod
a5979402ce Sidekiq needs to run inline now in specs 2025-02-24 08:53:51 +01:00
Angus McLeod
c8fbc194e9 Merge remote-tracking branch 'upstream/main' into add_username_change_integration_test 2025-02-24 08:48:08 +01:00
Angus McLeod
3975ce0780 Use shared_example for does not update username scenarios 2025-02-20 09:02:33 +01:00
Angus McLeod
e1ce48753d Add more checks and tests 2025-02-20 09:02:33 +01:00
Angus McLeod
5168786cf0 Fix failing specs 2025-02-20 09:02:33 +01:00
Angus McLeod
40ba0134a3 Add webfinger confirmation to username update scenario 2025-02-20 09:02:33 +01:00
Angus McLeod
c30914d20b Simplify ops assignment 2025-02-20 09:02:33 +01:00
Angus McLeod
95bb3d8fd7 Don't update non unique usernames on remote domains 2025-02-20 09:02:33 +01:00
Angus McLeod
24ac1c1204 Allow username updates for Update Actor 2025-02-20 09:02:33 +01:00
Angus McLeod
eb18e5df29 Change sidekiq inline invocation 2025-02-20 08:59:06 +01:00
Angus McLeod
b7768d9057 Remove Update and Follow from inbox integration test 2025-02-20 08:59:06 +01:00
Angus McLeod
ddd480bcad Update spec/requests/activitypub/inboxes_controller_spec.rb
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2025-02-20 08:59:05 +01:00
Angus McLeod
fa06f50432 Add more cases to search spec 2025-02-20 08:59:05 +01:00
Angus McLeod
cc7e4479b5 Improve and add Update and Follow tests 2025-02-20 08:59:05 +01:00
Angus McLeod
4199a0de62 Add remote username changed search integration test 2025-02-20 08:59:05 +01:00
Angus McLeod
e8a19a6ce6 Make username state explicit 2025-02-20 08:59:05 +01:00
Angus McLeod
1707c38dd6 Add integration test for when remote actor username changes 2025-02-20 08:59:05 +01:00
17 changed files with 613 additions and 34 deletions

View File

@ -1,6 +1,5 @@
// @ts-check
/**
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
*/
@ -64,6 +63,7 @@
* @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
* @property {Role?} role
* @property {string[]} features
*/
const element = document.getElementById('initial-state');
@ -140,4 +140,12 @@ export function getAccessToken() {
return getMeta('access_token');
}
/**
* @param {string} feature
* @returns {boolean}
*/
export function isFeatureEnabled(feature) {
return initialState?.features?.includes(feature) || false;
}
export default initialState;

View File

@ -219,6 +219,9 @@
"confirmations.delete_list.confirm": "Elimina",
"confirmations.delete_list.message": "Segur que vols suprimir permanentment aquesta llista?",
"confirmations.delete_list.title": "Eliminar la llista?",
"confirmations.discard_draft.confirm": "Descarta i continua",
"confirmations.discard_draft.edit.cancel": "Continua l'edició",
"confirmations.discard_draft.post.cancel": "Reprendre l'esborrany",
"confirmations.discard_edit_media.confirm": "Descarta",
"confirmations.discard_edit_media.message": "Tens canvis no desats en la descripció del contingut o en la previsualització, els vols descartar?",
"confirmations.follow_to_list.confirm": "Seguir i afegir a una llista",
@ -792,6 +795,7 @@
"report_notification.categories.violation": "Violació de norma",
"report_notification.categories.violation_sentence": "violació de normes",
"report_notification.open": "Obre l'informe",
"search.clear": "Esborra la cerca",
"search.no_recent_searches": "No hi ha cerques recents",
"search.placeholder": "Cerca",
"search.quick_action.account_search": "Perfils coincidint amb {x}",

View File

@ -572,7 +572,7 @@
"navigation_bar.mutes": "Skjulte brugere",
"navigation_bar.opened_in_classic_interface": "Indlæg, konti og visse andre sider åbnes som standard i den klassiske webgrænseflade.",
"navigation_bar.preferences": "Præferencer",
"navigation_bar.privacy_and_reach": "Fortrolighed og udbredelse",
"navigation_bar.privacy_and_reach": "Fortrolighed og rækkevidde",
"navigation_bar.search": "Søg",
"navigation_bar.search_trends": "Søg/Trender",
"navigation_panel.collapse_followed_tags": "Sammenfold menuen Fulgte hashtags",

View File

@ -20,7 +20,14 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
def update_account
return reject_payload! if @account.uri != object_uri
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, signed_with_known_key: true, request_id: @options[:request_id])
opts = {
signed_with_known_key: true,
request_id: @options[:request_id],
}
opts[:allow_username_update] = allow_username_update? if @account.username != @object['preferredUsername']
ActivityPub::ProcessAccountService.new.call(@account.username, @account.domain, @object, opts)
end
def update_status
@ -32,4 +39,26 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
end
def allow_username_update?
updated_username_unique? && updated_username_confirmed?
end
def updated_username_unique?
account_proxy = @account.dup
account_proxy.username = @object['preferredUsername']
UniqueUsernameValidator.new.validate(account_proxy)
account_proxy.errors.blank?
end
def updated_username_confirmed?
begin
webfinger = Webfinger.new("acct:#{@object['preferredUsername']}@#{@account.domain}").perform
rescue Webfinger::Error
return false
end
confirmed_username, confirmed_domain = webfinger.subject.delete_prefix('acct:').split('@')
confirmed_username == @object['preferredUsername'] && confirmed_domain == @account.domain
end
end

View File

@ -10,12 +10,6 @@ module DatabaseViewRecord
concurrently: true,
cascade: false
)
rescue ActiveRecord::StatementInvalid
Scenic.database.refresh_materialized_view(
table_name,
concurrently: false,
cascade: false
)
end
end

View File

@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts,
:media_attachments, :settings,
:languages
:languages, :features
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
@ -85,6 +85,10 @@ class InitialStateSerializer < ActiveModel::Serializer
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
end
def features
Mastodon::Feature.enabled_features
end
private
def default_meta_store

View File

@ -27,7 +27,7 @@ class ActivityPub::ProcessAccountService < BaseService
@options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"
with_redis_lock("process_account:#{@uri}") do
@account = Account.remote.find_by(uri: @uri) if @options[:only_key]
@account = Account.remote.find_by(uri: @uri) if find_remote_account_by_uri?
@account ||= Account.find_remote(@username, @domain)
@old_public_key = @account&.public_key
@old_protocol = @account&.protocol
@ -69,6 +69,10 @@ class ActivityPub::ProcessAccountService < BaseService
private
def find_remote_account_by_uri?
@options[:only_key] || @options[:allow_username_update]
end
def create_account
@account = Account.new
@account.protocol = :activitypub
@ -131,6 +135,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.indexable = @json['indexable'] || false
@account.memorial = @json['memorial'] || false
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
@account.username = @json['preferredUsername'] if @options[:allow_username_update]
end
def set_fetchable_key!

View File

@ -578,6 +578,11 @@ ca:
all: Totes
limited: Limitades
title: Moderació
moderation_notes:
create: Afegeix una nota de moderació
created_msg: S'ha creat la nota de moderació d'instància.
destroyed_msg: S'ha esborrat la nota de moderació d'instància.
title: Notes de moderació
private_comment: Comentari privat
public_comment: Comentari públic
purge: Purga
@ -1339,6 +1344,10 @@ ca:
basic_information: Informació bàsica
hint_html: "<strong>Personalitza el que la gent veu en el teu perfil públic i a prop dels teus tuts..</strong> És més probable que altres persones et segueixin i interaccionin amb tu quan tens emplenat el teu perfil i amb la teva imatge."
other: Altres
emoji_styles:
auto: Automàtic
native: Nadiu
twemoji: Twemoji
errors:
'400': La sol·licitud que vas emetre no era vàlida o no era correcta.
'403': No tens permís per a veure aquesta pàgina.

View File

@ -653,7 +653,7 @@ da:
mark_as_sensitive_description_html: Medierne i det anmeldte indlæg markeres som sensitive, og en advarsel (strike) registreres mhp. eskalering ved evt. fremtidige overtrædelser fra samme konto.
other_description_html: Se flere muligheder for at kontrollere kontoens adfærd og tilpasse kommunikationen til den anmeldte konto.
resolve_description_html: Ingen foranstaltninger træffes mod den anmeldte konto, ingen advarsel (strike) registreres og anmeldelsen lukkes.
silence_description_html: Kontoen vil kun være synlig for følgerene eller dem, som manuelt slå den op, hvilket markant begrænser dens udbredelse. Kan altid omgøres. Lukker alle indrapporteringer af kontoen.
silence_description_html: Kontoen vil kun være synlig for dem, der allerede følger den eller manuelt slår den op, hvilket alvorligt begrænser dens rækkevidde. Kan altid omgøres. Lukker alle indrapporteringer af denne konto.
suspend_description_html: Kontoen inkl. alt indhold utilgængeliggøres og interaktion umuliggøres, og den slettes på et tidspunkt. Kan omgøres inden for 30 dage. Lukker alle indrapporteringer af kontoen.
actions_description_html: Afgør, hvilke foranstaltning, der skal træffes for at løse denne anmeldelse. Ved en straffende foranstaltning mod den anmeldte konto, fremsendes en e-mailnotifikation, undtagen når kategorien <strong>Spam</strong> er valgt.
actions_description_remote_html: Fastslå en nødvendig handling mhp. at løse denne anmeldelse. Dette vil kun påvirke <strong>din</strong> servers kommunikation med, og indholdshåndtering for, fjernkontoen.
@ -1266,8 +1266,8 @@ da:
user_privacy_agreement_html: Jeg accepterer <a href="%{privacy_policy_path}" target="_blank">fortrolighedspolitikken</a>
author_attribution:
example_title: Eksempeltekst
hint_html: Skriver du nyheder eller blogartikler uden for Mastodon? Styr, hvordan man bliver krediteret, når disse deles på Mastodon.
instructions: 'Sørg for, at denne kode er i artikelens HTML:'
hint_html: Skriver du nyheder eller blogartikler uden for Mastodon? Styr, hvordan du bliver krediteret, når de bliver delt på Mastodon.
instructions: 'Sørg for, at denne kode er i din artikels HTML:'
more_from_html: Flere fra %{name}
s_blog: "%{name}s blog"
then_instructions: Tilføj dernæst publikationsdomænenavnet i feltet nedenfor.
@ -1718,11 +1718,11 @@ da:
hint_html: "<strong>Tilpas hvordan din profil og dine indlæg kan findes.</strong> En række funktioner i Mastodon kan hjælpe dig med at nå ud til et bredere publikum, hvis du aktiverer dem. Tjek indstillingerne herunder for at sikre, at de passer til dit brugsscenarie."
privacy: Privatliv
privacy_hint_html: Styr, hvor meget der ønskes synliggjort til gavn for andre. Folk finder interessante profiler og apps ved at tjekke andres følgere ud, samt se hvilke apps de sender fra, men dine præferencer ønskes muligvis ikke synliggjort.
reach: Udbredelse
reach: Rækkevidde
reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en?
search: Søg
search: Søgning
search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner.
title: Fortrolighed og udbredelse
title: Fortrolighed og rækkevidde
privacy_policy:
title: Privatlivspolitik
reactions:
@ -1923,7 +1923,7 @@ da:
'7889238': 3 måneder
min_age_label: Alderstærskel
min_favs: Behold indlæg favoritmarkeret mindst
min_favs_hint: Sletter ingen dine egne indlæg, som har modtaget minimum dette antal favoritmarkeringer. Lad stå tomt for at slette indlæg uanset antal favoritmarkeringer
min_favs_hint: Sletter ingen af dine egne indlæg, som har modtaget minimum dette antal favoritmarkeringer. Lad stå tom for at slette indlæg uanset antal favoritmarkeringer
min_reblogs: Behold indlæg fremhævet mindst
min_reblogs_hint: Sletter ingen af dine egne indlæg, som er fremhævet flere end dette antal gange. Lad stå tom for at slette indlæg uanset antallet af fremhævelser
stream_entries:
@ -2095,7 +2095,7 @@ da:
verification:
extra_instructions_html: <strong>Tip:</strong> Linket på din hjemmeside kan være usynligt. Den vigtige del er <code>rel="me"</code> , som forhindrer impersonation på websteder med brugergenereret indhold. Du kan endda bruge et <code>link</code> tag i overskriften på siden i stedet for <code>a</code>, men HTML skal være tilgængelig uden at udføre JavaScript.
here_is_how: Sådan gør du
hint_html: "<strong>Bekræftelse af din identitet på Mastodon er for alle.</strong> Baseret på åbne webstandarder, nu og for evigt gratis. Alt du behøver er en personlig hjemmeside, som folk genkende dig ved. Når du linker til denne hjemmeside fra din profil, vi vil kontrollere, at hjemmesiden linker tilbage til din profil og vise en visuel indikator på det."
hint_html: "<strong>Verificering af din identitet på Mastodon er for alle.</strong> Baseret på åbne webstandarder, nu og for altid gratis. Alt, hvad du behøver, er en personlig hjemmeside, som folk kender dig fra. Når du linker til denne hjemmeside fra din profil, kontrollerer vi, at hjemmesiden linker tilbage til din profil, og viser en visuel indikator på den."
instructions_html: Kopier og indsæt koden nedenfor i HTML på din hjemmeside. Tilføj derefter adressen på din hjemmeside i et af de ekstra felter på din profil på fanen "Redigér profil" og gem ændringer.
verification: Bekræftelse
verified_links: Dine bekræftede links

View File

@ -1349,6 +1349,10 @@ hu:
basic_information: Általános információk
hint_html: "<strong>Tedd egyedivé, mi látnak mások a profilodon és a bejegyzéseid mellett.</strong> Mások nagyobb eséllyel követnek vissza és lépnek veled kapcsolatba, ha van kitöltött profilod és profilképed."
other: Egyéb
emoji_styles:
auto: Automatikus
native: Natív
twemoji: Twemoji
errors:
'400': A küldött kérés érvénytelen vagy hibás volt.
'403': Nincs jogosultságod az oldal megtekintéséhez.

View File

@ -61,6 +61,7 @@ ca:
setting_display_media_default: Amaga el contingut gràfic marcat com a sensible
setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia
setting_display_media_show_all: Mostra sempre el contingut gràfic
setting_emoji_style: Com mostrar els emojis. "Automàtic" provarà de fer servir els emojis nadius, però revertirà a twemojis en els navegadors antics.
setting_system_scrollbars_ui: S'aplica només als navegadors d'escriptori basats en Safari i Chrome
setting_use_blurhash: Els degradats es basen en els colors de les imatges ocultes, però n'enfosqueixen els detalls
setting_use_pending_items: Amaga les actualitzacions de la línia de temps després de fer un clic, en lloc de desplaçar-les automàticament
@ -240,6 +241,7 @@ ca:
setting_display_media_default: Per defecte
setting_display_media_hide_all: Amaga-ho tot
setting_display_media_show_all: Mostra-ho tot
setting_emoji_style: Estil d'emojis
setting_expand_spoilers: Desplega sempre els tuts marcats amb advertències de contingut
setting_hide_network: Amaga la teva xarxa
setting_missing_alt_text_modal: Mostra un diàleg de confirmació abans de publicar contingut sense text alternatiu

View File

@ -61,6 +61,7 @@ hu:
setting_display_media_default: Kényes tartalomnak jelölt média elrejtése
setting_display_media_hide_all: Média elrejtése mindig
setting_display_media_show_all: Média megjelenítése mindig
setting_emoji_style: Az emodzsik megjelenítési módja. Az „Automatikus” megpróbálja a natív emodzsikat használni, de az örökölt böngészők esetén a Twemojira vált vissza.
setting_system_scrollbars_ui: Csak Chrome és Safari alapú asztali böngészőkre vonatkozik
setting_use_blurhash: A kihomályosítás az eredeti képből történik, de minden részletet elrejt
setting_use_pending_items: Idővonal frissítése csak kattintásra automatikus görgetés helyett
@ -241,6 +242,7 @@ hu:
setting_display_media_default: Alapértelmezés
setting_display_media_hide_all: Mindent elrejt
setting_display_media_show_all: Mindent mutat
setting_emoji_style: Emodzsistílus
setting_expand_spoilers: Tartalmi figyelmeztetéssel ellátott bejegyzések automatikus kinyitása
setting_hide_network: Hálózatod elrejtése
setting_missing_alt_text_modal: Megerősítési párbeszédablak megjelenítése a helyettesítő szöveg nélküli média közzététele előtt

View File

@ -55,13 +55,122 @@ RSpec.describe ActivityPub::Activity::Update do
stub_request(:get, actor_json[:following]).to_return(status: 404)
stub_request(:get, actor_json[:featured]).to_return(status: 404)
stub_request(:get, actor_json[:featuredTags]).to_return(status: 404)
subject.perform
end
it 'updates profile' do
subject.perform
expect(sender.reload.display_name).to eq 'Totally modified now'
end
context 'when Actor username changes' do
let!(:original_username) { sender.username }
let!(:original_handle) { "#{original_username}@#{sender.domain}" }
let!(:updated_username) { 'updated_username' }
let!(:updated_handle) { "#{updated_username}@#{sender.domain}" }
let(:updated_username_json) { actor_json.merge(preferredUsername: updated_username) }
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'foo',
type: 'Update',
actor: sender.uri,
object: updated_username_json,
}.with_indifferent_access
end
before do
stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 404)
end
context 'when updated username is unique and confirmed' do
before do
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{updated_handle}")
.to_return(
body: {
subject: "acct:#{updated_handle}",
links: [
{
rel: 'self',
type: 'application/activity+json',
href: sender.uri,
},
],
}.to_json,
headers: {
'Content-Type' => 'application/json',
},
status: 200
)
end
it 'updates profile' do
subject.perform
expect(sender.reload.display_name).to eq 'Totally modified now'
end
it 'updates username' do
subject.perform
expect(sender.reload.username).to eq updated_username
end
end
shared_examples 'does not update username' do
it 'updates profile' do
subject.perform
expect(sender.reload.display_name).to eq 'Totally modified now'
end
it 'does not update username' do
subject.perform
expect(sender.reload.username).to eq original_username
end
end
context 'when updated username is not unique for domain' do
before do
Fabricate(:account,
username: updated_username,
domain: 'example.com',
inbox_url: "https://example.com/#{updated_username}/inbox",
outbox_url: "https://example.com/#{updated_username}/outbox")
end
include_examples 'does not update username'
end
context 'when webfinger of updated username does not contain updated username' do
before do
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{updated_handle}")
.to_return(
body: {
subject: "acct:#{original_handle}",
links: [
{
rel: 'self',
type: 'application/activity+json',
href: sender.uri,
},
],
}.to_json,
headers: {
'Content-Type' => 'application/json',
},
status: 200
)
end
include_examples 'does not update username'
end
context 'when webfinger request of updated username fails' do
before do
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:#{updated_handle}")
.to_return(status: 404)
end
include_examples 'does not update username'
end
end
end
context 'with a Question object' do

View File

@ -0,0 +1,210 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::InboxesController, :sidekiq_inline do
let!(:current_datetime) { 'Wed, 20 Dec 2023 10:00:00 GMT' }
let!(:remote_actor_keypair) do
OpenSSL::PKey.read(<<~PEM_TEXT)
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI
eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn
FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F
jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn
qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar
+BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3
fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd
RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC
I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh
FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk
QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu
ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC
STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO
L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6
BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7
gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X
8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3
qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE
cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo
zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3
lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F
rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza
GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE
+JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO
4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb
-----END RSA PRIVATE KEY-----
PEM_TEXT
end
let!(:remote_actor_inbox_url) { 'https://remote.domain/users/bob/inbox' }
let!(:remote_actor_original_username) { 'original_username' }
let!(:remote_actor) do
Fabricate(:account,
domain: 'remote.domain',
uri: 'https://remote.domain/users/bob',
private_key: nil,
public_key: remote_actor_keypair.public_key.to_pem,
username: remote_actor_original_username,
protocol: :activitypub,
inbox_url: remote_actor_inbox_url)
end
let!(:local_actor) { Fabricate(:account) }
let!(:base_headers) do
{
'Host' => 'www.remote.domain',
'Date' => current_datetime,
}
end
let!(:note_content) { 'note from remote actor' }
let!(:object_json) do
{
id: 'https://remote.domain/activities/objects/1',
type: 'Note',
content: note_content,
to: ActivityPub::TagManager.instance.uri_for(local_actor),
}
end
before do
travel_to current_datetime
end
context 'when remote actor username has changed' do
let(:remote_actor_new_username) { 'new_username' }
let(:remote_actor_new_handle) { "#{remote_actor_new_username}@#{remote_actor.domain}" }
let(:updated_remote_actor_json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: remote_actor.uri,
type: 'Person',
preferredUsername: remote_actor_new_username,
inbox: remote_actor.inbox_url,
publicKey: {
id: "#{remote_actor.uri}#main-key",
owner: remote_actor.uri,
publicKeyPem: remote_actor.public_key,
},
}.with_indifferent_access
end
let(:remote_actor_webfinger_response) do
{
subject: "acct:#{remote_actor_new_handle}",
links: [
{
rel: 'self',
type: 'application/activity+json',
href: remote_actor.uri,
},
],
}
end
before do
stub_request(:get, 'https://remote.domain/users/bob#main-key')
.to_return(
body: updated_remote_actor_json.to_json,
headers: {
'Content-Type' => 'application/activity+json',
},
status: 200
)
stub_request(:get, 'https://remote.domain/users/bob')
.to_return(
body: updated_remote_actor_json.to_json,
headers: {
'Content-Type' => 'application/activity+json',
},
status: 200
)
stub_request(:get, "https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_new_handle}")
.to_return(
body: remote_actor_webfinger_response.to_json,
headers: {
'Content-Type' => 'application/json',
},
status: 200
)
Sidekiq::Testing.inline!
end
context 'with a create note' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://remote.domain/activities/create/1',
type: 'Create',
actor: remote_actor.uri,
object: object_json,
}.with_indifferent_access
end
let(:digest_header) { digest_value(json.to_json) }
let(:signature_header) do
build_signature_string(
remote_actor_keypair,
'https://remote.domain/users/bob#main-key',
"post /users/#{local_actor.username}/inbox",
base_headers.merge(
'Digest' => digest_header
)
)
end
let(:headers) do
base_headers.merge(
'Digest' => digest_header,
'Signature' => signature_header
)
end
it 'creates the note' do
post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers
expect(response).to have_http_status(202)
expect(Status.exists?(uri: object_json[:id])).to be(true)
end
it 'does not change the local record of the remote actor' do
post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers
expect(remote_actor.reload.username).to eq(remote_actor_original_username)
end
end
context 'with an update actor' do
let(:json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: 'https://remote.domain/activities/update/1',
type: 'Update',
actor: remote_actor.uri,
object: updated_remote_actor_json,
}.with_indifferent_access
end
let(:digest_header) { digest_value(json.to_json) }
let(:signature_header) do
build_signature_string(
remote_actor_keypair,
'https://remote.domain/users/bob#main-key',
"post /users/#{local_actor.username}/inbox",
base_headers.merge(
'Digest' => digest_header
)
)
end
let(:headers) do
base_headers.merge(
'Digest' => digest_header,
'Signature' => signature_header
)
end
it 'does not increase the number of accounts' do
expect do
post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers
end.to(not_change { Account.count })
end
it 'updates the remote actors username' do
post "/users/#{local_actor.username}/inbox", params: json.to_json, headers: headers
expect(response).to have_http_status(202)
expect(remote_actor.reload.username).to eq(remote_actor_new_username)
end
end
end
end

View File

@ -93,6 +93,205 @@ RSpec.describe 'Search API' do
expect(response.parsed_body[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
end
end
context 'when a remote actor username has changed' do
let!(:remote_actor_keypair) do
OpenSSL::PKey.read(<<~PEM_TEXT)
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqIAYvNFGbZ5g4iiK6feSdXD4bDStFM58A7tHycYXaYtzZQpI
eHXAmaXuZzXIwtrP4N0gIk8JNwZvXj2UPS+S07t0V9wNK94he01LV5EMz/GN4eNn
FmDL64HIEuKLvV8TvgjbUPRD6Y5X0UpKi2ZIFLSb96Q5w0Z/k7ntpVKV52y8kz5F
jr/O/0JuHryZe0yItzJh8kzFfeMf0EXzfSnaKvT7P9jhgC6uTre+jXyvVZjiHDrn
qvvucdI3I7DRfXo1OqARBrLjy+TdseUAjNYJ+OuPRI1URIWQI01DCHqcohVu9+Ar
+BiCjFp3ua+XMuJvrvbD61d1Fvig/9nbBRR+8QIDAQABAoIBAAgySHnFWI6gItR3
fkfiqIm80cHCN3Xk1C6iiVu+3oBOZbHpW9R7vl9e/WOA/9O+LPjiSsQOegtWnVvd
RRjrl7Hj20VDlZKv5Mssm6zOGAxksrcVbqwdj+fUJaNJCL0AyyseH0x/IE9T8rDC
I1GH+3tB3JkhkIN/qjipdX5ab8MswEPu8IC4ViTpdBgWYY/xBcAHPw4xuL0tcwzh
FBlf4DqoEVQo8GdK5GAJ2Ny0S4xbXHUURzx/R4y4CCts7niAiLGqd9jmLU1kUTMk
QcXfQYK6l+unLc7wDYAz7sFEHh04M48VjWwiIZJnlCqmQbLda7uhhu8zkF1DqZTu
ulWDGQECgYEA0TIAc8BQBVab979DHEEmMdgqBwxLY3OIAk0b+r50h7VBGWCDPRsC
STD73fQY3lNet/7/jgSGwwAlAJ5PpMXxXiZAE3bUwPmHzgF7pvIOOLhA8O07tHSO
L2mvQe6NPzjZ+6iAO2U9PkClxcvGvPx2OBvisfHqZLmxC9PIVxzruQECgYEAzjM6
BTUXa6T/qHvLFbN699BXsUOGmHBGaLRapFDBfVvgZrwqYQcZpBBhesLdGTGSqwE7
gWsITPIJ+Ldo+38oGYyVys+w/V67q6ud7hgSDTW3hSvm+GboCjk6gzxlt9hQ0t9X
8vfDOYhEXvVUJNv3mYO60ENqQhILO4bQ0zi+VfECgYBb/nUccfG+pzunU0Cb6Dp3
qOuydcGhVmj1OhuXxLFSDG84Tazo7juvHA9mp7VX76mzmDuhpHPuxN2AzB2SBEoE
cSW0aYld413JRfWukLuYTc6hJHIhBTCRwRQFFnae2s1hUdQySm8INT2xIc+fxBXo
zrp+Ljg5Wz90SAnN5TX0AQKBgDaatDOq0o/r+tPYLHiLtfWoE4Dau+rkWJDjqdk3
lXWn/e3WyHY3Vh/vQpEqxzgju45TXjmwaVtPATr+/usSykCxzP0PMPR3wMT+Rm1F
rIoY/odij+CaB7qlWwxj0x/zRbwB7x1lZSp4HnrzBpxYL+JUUwVRxPLIKndSBTza
GvVRAoGBAIVBcNcRQYF4fvZjDKAb4fdBsEuHmycqtRCsnkGOz6ebbEQznSaZ0tZE
+JuouZaGjyp8uPjNGD5D7mIGbyoZ3KyG4mTXNxDAGBso1hrNDKGBOrGaPhZx8LgO
4VXJ+ybXrATf4jr8ccZYsZdFpOphPzz+j55Mqg5vac5P1XjmsGTb
-----END RSA PRIVATE KEY-----
PEM_TEXT
end
let!(:remote_actor_inbox_url) { 'https://remote.domain/users/bob/inbox' }
let!(:remote_actor_original_username) { 'original_username' }
let!(:remote_actor) do
Fabricate(:account,
domain: 'remote.domain',
uri: 'https://remote.domain/users/bob',
private_key: nil,
public_key: remote_actor_keypair.public_key.to_pem,
username: remote_actor_original_username,
protocol: 1, # activitypub
inbox_url: remote_actor_inbox_url)
end
let!(:remote_actor_old_handle) { "#{remote_actor_original_username}@remote.domain" }
let!(:remote_actor_new_username) { 'new_username' }
let!(:remote_actor_json) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: remote_actor.uri,
type: 'Person',
preferredUsername: remote_actor_new_username,
inbox: remote_actor.inbox_url,
publicKey: {
id: "#{remote_actor.uri}#main-key",
owner: remote_actor.uri,
publicKeyPem: remote_actor.public_key,
},
}.with_indifferent_access
end
let!(:remote_actor_new_handle) { "#{remote_actor_new_username}@remote.domain" }
let(:webfinger_response) do
{
subject: "acct:#{remote_actor_new_handle}",
links: [
{
rel: 'self',
type: 'application/activity+json',
href: remote_actor.uri,
},
],
}
end
before do
sign_in(user)
tom.follow!(remote_actor)
stub_request(:get, "https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_new_handle}")
.to_return(
body: webfinger_response.to_json,
headers: {
'Content-Type' => 'application/json',
},
status: 200
)
stub_request(:get, remote_actor.uri)
.to_return(
body: remote_actor_json.to_json,
headers: {
'Content-Type' => 'application/activity+json',
},
status: 200
)
Sidekiq::Testing.inline!
end
context 'when requesting the old handle' do
let!(:params) { { q: remote_actor_old_handle, resolve: '1' } }
it 'does not increase the number of accounts' do
expect do
get '/api/v2/search', headers: headers, params: params
end.to(not_change { Account.count })
end
it 'does not change the remote actor account' do
get '/api/v2/search', headers: headers, params: params
expect(remote_actor.reload.username).to eq(remote_actor_original_username)
end
it 'returns the remote actor account' do
get '/api/v2/search', headers: headers, params: params
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(remote_actor.id.to_s)
end
end
context 'when requesting the old handle of a stale account' do
let!(:params) { { q: remote_actor_old_handle, resolve: '1' } }
before do
stub_request(:get, 'https://remote.domain/.well-known/host-meta').to_return(status: 404)
remote_actor.update(last_webfingered_at: 2.days.ago)
end
it 'makes a webfinger request with the old handle' do
stub_request(:get, "https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_old_handle}")
get '/api/v2/search', headers: headers, params: params
expect(
a_request(
:get,
"https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_old_handle}"
)
).to have_been_made.once
end
it 'does nothing if the webfinger request returns not found' do
stub_request(:get, "https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_old_handle}")
.to_return(
status: 404
)
get '/api/v2/search', headers: headers, params: params
expect(body_as_json[:accounts].empty?).to be(true)
expect(remote_actor.reload.username).to eq(remote_actor_original_username)
end
it 'merges the old account with the new account if the webfinger request succeeds' do
stub_request(:get, "https://remote.domain/.well-known/webfinger?resource=acct:#{remote_actor_old_handle}")
.to_return(
body: {
subject: "acct:#{remote_actor_old_handle}",
links: [
{
rel: 'self',
type: 'application/activity+json',
href: remote_actor.uri,
},
],
}.to_json,
headers: {
'Content-Type' => 'application/json',
},
status: 200
)
expect do
get '/api/v2/search', headers: headers, params: params
end.to(not_change { Account.count })
expect(Account.exists?(id: remote_actor.id)).to be(false)
new_remote_actor = Account.find_by(
uri: remote_actor.uri,
username: remote_actor_new_username
)
expect(new_remote_actor.present?).to be(true)
expect(tom.following?(new_remote_actor)).to be(true)
end
end
context 'when requesting the new handle' do
let(:params) { { q: remote_actor_new_handle, resolve: '1' } }
it 'does not increase the number of accounts' do
expect do
get '/api/v2/search', headers: headers, params: params
end.to(not_change { Account.count })
end
it 'merges the old account with the new account' do
get '/api/v2/search', headers: headers, params: params
expect(Account.exists?(id: remote_actor.id)).to be(false)
new_remote_actor = Account.find_by(
uri: remote_actor.uri,
username: remote_actor_new_username
)
expect(new_remote_actor.present?).to be(true)
expect(tom.following?(new_remote_actor)).to be(true)
end
end
end
end
context 'when search raises syntax error' do

View File

@ -707,17 +707,4 @@ RSpec.describe 'signature verification concern' do
alias_method :signature_required, :success
end
end
def digest_value(body)
"SHA-256=#{Digest::SHA256.base64digest(body)}"
end
def build_signature_string(keypair, key_id, request_target, headers)
algorithm = 'rsa-sha256'
signed_headers = headers.merge({ '(request-target)' => request_target })
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
end
end

View File

@ -1,6 +1,19 @@
# frozen_string_literal: true
module SignedRequestHelpers
def digest_value(body)
"SHA-256=#{Digest::SHA256.base64digest(body)}"
end
def build_signature_string(keypair, key_id, request_target, headers)
algorithm = 'rsa-sha256'
signed_headers = headers.merge({ '(request-target)' => request_target })
signed_string = signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
signature = Base64.strict_encode64(keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
end
def get(path, headers: nil, sign_with: nil, **args)
return super(path, headers: headers, **args) if sign_with.nil?