mirror of
https://github.com/mastodon/mastodon.git
synced 2024-08-20 21:08:15 -07:00
Compare commits
19 commits
e6a5aaf149
...
532228e4f9
Author | SHA1 | Date | |
---|---|---|---|
|
532228e4f9 | ||
|
a50c8e951f | ||
|
2c1e75727d | ||
|
d7cd60dfdd | ||
|
ceb9c8ff84 | ||
|
26499e5096 | ||
|
60ba6d1c82 | ||
|
aed85da24b | ||
|
138fee197c | ||
|
0c75781cfe | ||
|
dbe2282153 | ||
|
91a76ad162 | ||
|
cf6fd76c3c | ||
|
a3d78540bd | ||
|
ff873ebbf2 | ||
|
f6307d2c3d | ||
|
acba3f1121 | ||
|
9ea6ccd322 | ||
|
64c7ff7c11 |
12 changed files with 578 additions and 50 deletions
|
@ -60,7 +60,7 @@ export interface BaseNotificationGroupJSON {
|
||||||
|
|
||||||
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||||
type: NotificationWithStatusType;
|
type: NotificationWithStatusType;
|
||||||
status: ApiStatusJSON;
|
status_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
||||||
|
|
|
@ -49,21 +49,14 @@ export const FilteredNotificationsBanner: React.FC = () => {
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='filtered_notifications_banner.pending_requests'
|
id='filtered_notifications_banner.pending_requests'
|
||||||
defaultMessage='Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know'
|
defaultMessage='From {count, plural, =0 {no one} one {one person} other {# people}} you may know'
|
||||||
values={{ count: policy.summary.pending_requests_count }}
|
values={{ count: policy.summary.pending_requests_count }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='filtered-notifications-banner__badge'>
|
<div className='filtered-notifications-banner__badge'>
|
||||||
<div className='filtered-notifications-banner__badge__badge'>
|
{toCappedNumber(policy.summary.pending_notifications_count)}
|
||||||
{toCappedNumber(policy.summary.pending_notifications_count)}
|
|
||||||
</div>
|
|
||||||
<FormattedMessage
|
|
||||||
id='filtered_notifications_banner.mentions'
|
|
||||||
defaultMessage='{count, plural, one {mention} other {mentions}}'
|
|
||||||
values={{ count: policy.summary.pending_notifications_count }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -300,8 +300,7 @@
|
||||||
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
|
||||||
"filter_modal.select_filter.title": "Filter this post",
|
"filter_modal.select_filter.title": "Filter this post",
|
||||||
"filter_modal.title.status": "Filter a post",
|
"filter_modal.title.status": "Filter a post",
|
||||||
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
|
"filtered_notifications_banner.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know",
|
||||||
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know",
|
|
||||||
"filtered_notifications_banner.title": "Filtered notifications",
|
"filtered_notifications_banner.title": "Filtered notifications",
|
||||||
"firehose.all": "All",
|
"firehose.all": "All",
|
||||||
"firehose.local": "This server",
|
"firehose.local": "This server",
|
||||||
|
|
|
@ -124,9 +124,9 @@ export function createNotificationGroupFromJSON(
|
||||||
case 'mention':
|
case 'mention':
|
||||||
case 'poll':
|
case 'poll':
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const { status, ...groupWithoutStatus } = group;
|
const { status_id: statusId, ...groupWithoutStatus } = group;
|
||||||
return {
|
return {
|
||||||
statusId: status.id,
|
statusId,
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
...groupWithoutStatus,
|
...groupWithoutStatus,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10171,25 +10171,10 @@ noscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
&__badge {
|
&__badge {
|
||||||
display: flex;
|
background: $ui-button-background-color;
|
||||||
align-items: center;
|
color: $white;
|
||||||
border-radius: 999px;
|
border-radius: 100px;
|
||||||
background: var(--background-border-color);
|
padding: 2px 8px;
|
||||||
color: $darker-text-color;
|
|
||||||
padding: 4px;
|
|
||||||
padding-inline-end: 8px;
|
|
||||||
gap: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 16px;
|
|
||||||
word-break: keep-all;
|
|
||||||
|
|
||||||
&__badge {
|
|
||||||
background: $ui-button-background-color;
|
|
||||||
color: $white;
|
|
||||||
border-radius: 100px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,14 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||||
def update_account
|
def update_account
|
||||||
return reject_payload! if @account.uri != object_uri
|
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
|
end
|
||||||
|
|
||||||
def update_status
|
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])
|
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
@options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"
|
@options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"
|
||||||
|
|
||||||
with_redis_lock("process_account:#{@uri}") do
|
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)
|
@account ||= Account.find_remote(@username, @domain)
|
||||||
@old_public_key = @account&.public_key
|
@old_public_key = @account&.public_key
|
||||||
@old_protocol = @account&.protocol
|
@old_protocol = @account&.protocol
|
||||||
|
@ -67,6 +67,10 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def find_remote_account_by_uri?
|
||||||
|
@options[:only_key] || @options[:allow_username_update]
|
||||||
|
end
|
||||||
|
|
||||||
def create_account
|
def create_account
|
||||||
@account = Account.new
|
@account = Account.new
|
||||||
@account.protocol = :activitypub
|
@account.protocol = :activitypub
|
||||||
|
@ -117,6 +121,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||||
@account.discoverable = @json['discoverable'] || false
|
@account.discoverable = @json['discoverable'] || false
|
||||||
@account.indexable = @json['indexable'] || false
|
@account.indexable = @json['indexable'] || false
|
||||||
@account.memorial = @json['memorial'] || false
|
@account.memorial = @json['memorial'] || false
|
||||||
|
@account.username = @json['preferredUsername'] if @options[:allow_username_update]
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_fetchable_key!
|
def set_fetchable_key!
|
||||||
|
|
|
@ -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[:following]).to_return(status: 404)
|
||||||
stub_request(:get, actor_json[:featured]).to_return(status: 404)
|
stub_request(:get, actor_json[:featured]).to_return(status: 404)
|
||||||
stub_request(:get, actor_json[:featuredTags]).to_return(status: 404)
|
stub_request(:get, actor_json[:featuredTags]).to_return(status: 404)
|
||||||
|
|
||||||
subject.perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates profile' do
|
it 'updates profile' do
|
||||||
|
subject.perform
|
||||||
expect(sender.reload.display_name).to eq 'Totally modified now'
|
expect(sender.reload.display_name).to eq 'Totally modified now'
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'with a Question object' do
|
context 'with a Question object' do
|
||||||
|
|
209
spec/requests/activitypub/inboxes_controller_spec.rb
Normal file
209
spec/requests/activitypub/inboxes_controller_spec.rb
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
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
|
|
@ -83,6 +83,205 @@ describe 'Search API' do
|
||||||
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
|
expect(body_as_json[:accounts].pluck(:id)).to contain_exactly(ana.id.to_s)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'when search raises syntax error' do
|
context 'when search raises syntax error' do
|
||||||
|
|
|
@ -382,17 +382,4 @@ describe 'signature verification concern' do
|
||||||
alias_method :signature_required, :success
|
alias_method :signature_required, :success
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SignedRequestHelpers
|
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)
|
def get(path, headers: nil, sign_with: nil, **args)
|
||||||
return super(path, headers: headers, **args) if sign_with.nil?
|
return super(path, headers: headers, **args) if sign_with.nil?
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue