Skip to content

Commit

Permalink
when creating a new ATProto user, add a DNS _atproto TXT record for t…
Browse files Browse the repository at this point in the history
…heir handle
  • Loading branch information
snarfed committed Oct 4, 2023
1 parent e96ab46 commit 000b08f
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 9 deletions.
24 changes: 22 additions & 2 deletions atproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from arroba.storage import Action, CommitData
from arroba.util import at_uri, next_tid, parse_at_uri, service_jwt
from flask import abort, request
from google.cloud import dns
from google.cloud import ndb
from granary import as1, bluesky
from lexrpc import Client
Expand All @@ -38,6 +39,12 @@

LEXICONS = Client('https://unused').defs

DNS_GCP_PROJECT = 'brid-gy'
DNS_ZONE = 'brid-gy'
DNS_TTL = 10800 # seconds
logger.info(f'Using GCP DNS project {DNS_GCP_PROJECT} zone {DNS_ZONE}')
dns_client = dns.Client(project=DNS_GCP_PROJECT)


class ATProto(User, Protocol):
"""AT Protocol class.
Expand Down Expand Up @@ -203,6 +210,20 @@ def create_for(cls, user):
Object.get_or_create(did_plc.did, raw=did_plc.doc)
user.atproto_did = did_plc.did
add(user.copies, Target(uri=did_plc.did, protocol='atproto'))
handle = user.handle_as('atproto')

# create _atproto DNS record for handle resolution
# https://atproto.com/specs/handle#handle-resolution
name = f'_atproto.{handle}.'
val = f'"did={did_plc.did}"'
logger.info(f'adding GCP DNS TXT record for {name} {val}')
zone = dns_client.zone(DNS_ZONE)
r = zone.resource_record_set(name=name, record_type='TXT', ttl=DNS_TTL,
rrdatas=[val])
changes = zone.changes()
changes.add_record_set(r)
changes.create()
logger.info(' done!')

# fetch and store profile
if not user.obj:
Expand All @@ -219,8 +240,7 @@ def create_for(cls, user):
user.obj.put()

repo = Repo.create(
arroba.server.storage, user.atproto_did,
handle=user.handle_as('atproto'),
arroba.server.storage, user.atproto_did, handle=handle,
callback=lambda _: common.create_task(queue='atproto-commit'),
initial_writes=initial_writes,
signing_key=did_plc.signing_key,
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ google-cloud-appengine-logging==1.3.1
google-cloud-audit-log==0.2.5
google-cloud-core==2.3.2
google-cloud-datastore==2.16.1
google-cloud-dns==0.34.1
google-cloud-error-reporting==1.9.2
google-cloud-logging==3.7.0
google-cloud-ndb==2.2.2
Expand Down
21 changes: 16 additions & 5 deletions tests/test_atproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import copy
import logging
from unittest import skip
from unittest.mock import call, patch
from unittest.mock import call, MagicMock, patch

from arroba.datastore_storage import AtpBlock, AtpRepo, DatastoreStorage
from arroba.did import encode_did_key
Expand Down Expand Up @@ -265,13 +265,17 @@ def test_profile_id(self, mock_get):
self.assertEqual('at://did:plc:foo/app.bsky.actor.profile/self',
self.make_user('did:plc:foo', cls=ATProto).profile_id())

@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_create_for(self, mock_post, mock_create_task):
Fake.fetchable = {'fake:user': ACTOR_AS}
def test_create_for(self, mock_post, mock_create_task, mock_zone):
mock_zone.return_value = zone = MagicMock()
zone.resource_record_set = MagicMock()

Fake.fetchable = {'fake:user': ACTOR_AS}
user = Fake(id='fake:user')

ATProto.create_for(user)

# check user, repo
Expand All @@ -280,6 +284,11 @@ def test_create_for(self, mock_post, mock_create_task):
user.copies)
repo = arroba.server.storage.load_repo(user.atproto_did)

# check DNS record
zone.resource_record_set.assert_called_with(
name='_atproto.fake:handle:user.fa.brid.gy.', record_type='TXT',
ttl=atproto.DNS_TTL, rrdatas=[f'"did={user.atproto_did}"'])

# check profile record
profile = repo.get_record('app.bsky.actor.profile', 'self')
self.assertEqual({
Expand All @@ -294,10 +303,11 @@ def test_create_for(self, mock_post, mock_create_task):

mock_create_task.assert_called()

@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_send_new_repo(self, mock_post, mock_create_task):
def test_send_new_repo(self, mock_post, mock_create_task, _):
user = self.make_user(id='fake:user', cls=Fake)
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1={
**POST_AS,
Expand Down Expand Up @@ -355,10 +365,11 @@ def test_send_new_repo(self, mock_post, mock_create_task):
self.assert_task(mock_create_task, 'atproto-commit',
'/queue/atproto-commit')

@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_send_new_repo_includes_user_profile(self, mock_post, mock_create_task):
def test_send_new_repo_includes_user_profile(self, mock_post, mock_create_task, _):
user = self.make_user(id='fake:user', cls=Fake, obj_as1=ACTOR_AS)
obj = self.store_object(id='fake:post', source_protocol='fake', our_as1={
**POST_AS,
Expand Down
3 changes: 2 additions & 1 deletion tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ def test_get_or_create(self):
user.direct = True
self.assert_entities_equal(same, user, ignore=['updated'])

@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_get_or_create_propagate(self, mock_post, mock_create_task):
def test_get_or_create_propagate(self, mock_post, mock_create_task, _):
Fake.fetchable = {'fake:user': ACTOR_AS}

user = Fake.get_or_create('fake:user', propagate=True)
Expand Down
3 changes: 2 additions & 1 deletion tests/test_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,11 @@ def test_feed_rss(self):
# COMMENT's author
self.assertIn('Dr. Eve', got.text)

@patch('google.cloud.dns.client.ManagedZone', autospec=True)
@patch.object(tasks_client, 'create_task', return_value=Task(name='my task'))
@patch('requests.post',
return_value=requests_response('OK')) # create DID on PLC
def test_bridge_user(self, mock_post, mock_create_task):
def test_bridge_user(self, mock_post, mock_create_task, _):
Fake.fetchable = {'fake:user': ACTOR_AS}

got = self.client.post('/bridge-user', data={'handle': 'fake:handle:user'})
Expand Down

0 comments on commit 000b08f

Please sign in to comment.