diff --git a/atproto.py b/atproto.py index fe2a12fc..4ed80d70 100644 --- a/atproto.py +++ b/atproto.py @@ -163,20 +163,24 @@ def resolve_handle(self, handle=None): return {'did': got.key.id()} -def did_to_handle(did): - """Resolves a DID to a handle _if_ we have the DID doc stored locally. +def did_to_handle(did, remote=None): + """Resolves a DID to a handle. Args: did (str) Returns: str: handle, or None + remote (bool): whether to fetch the object over the network. See + :meth:`Protocol.load` """ - if did_obj := ATProto.load(did, did_doc=True): - if aka := util.get_first(did_obj.raw, 'alsoKnownAs', ''): - handle, _, _ = parse_at_uri(aka) - if handle: - return handle + if did_obj := ATProto.load(did, did_doc=True, remote=remote): + # use first at:// URI in alsoKnownAs + for aka in util.get_list(did_obj.raw, 'alsoKnownAs'): + if aka.startswith('at://'): + handle, _, _ = parse_at_uri(aka) + if handle: + return handle class Cursor(StringIdModel): @@ -495,7 +499,7 @@ def set_username(to_cls, user, username): # resolve_handle checks that username is a valid domain resolved = did.resolve_handle(username, get_fn=util.requests_get) if resolved != copy_did: - raise RuntimeError(f"""Sure! You'll need to connect that domain to your bridged Bluesky account, either with DNS or HTTP. Once you're done, check your work here, then DM me username {username} again.""") + raise RuntimeError(f"""You'll need to connect that domain to your bridged Bluesky account, either with DNS or HTTP. Once you're done, check your work here, then DM me username {username} again.""") try: repo = arroba.server.storage.load_repo(copy_did) @@ -509,6 +513,9 @@ def set_username(to_cls, user, username): get_fn=util.requests_get, post_fn=util.requests_post) arroba.server.storage.write_event(repo=repo, type='identity', handle=username) + # refresh our stored DID doc + user.obj = to_cls.load(copy_did, did_doc=True, remote=True) + @classmethod def send(to_cls, obj, url, from_user=None, orig_obj_id=None): """Creates a record if we own its repo. diff --git a/models.py b/models.py index a2acd1b2..c9241be8 100644 --- a/models.py +++ b/models.py @@ -600,6 +600,13 @@ def handle_as(self, to_proto): if isinstance(to_proto, str): to_proto = PROTOCOLS[to_proto] + # override to-ATProto to use custom domain handle in DID doc + from atproto import ATProto, did_to_handle + if to_proto == ATProto: + if did := self.get_copy(ATProto): + if handle := did_to_handle(did, remote=False): + return handle + # override web users to always use domain instead of custom username # TODO: fall back to id if handle is unset? handle = self.key.id() if self.LABEL == 'web' else self.handle @@ -798,8 +805,7 @@ def user_link(self, name=True, handle=True, pictures=False, proto=None, img = f' ' if handle: - handle_str = ids.translate_handle(handle=self.handle, from_=self, - to=proto, enhanced=False) + handle_str = self.handle_as(proto) if name and self.name() != handle_str: name_str = self.name() diff --git a/tests/test_atproto.py b/tests/test_atproto.py index be6e5742..3b6eec2e 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -1216,6 +1216,11 @@ def test_set_dns_existing(self, mock_rrsets, mock_changes): 'cid': 'orig', 'operation': {'alsoKnownAs': ['at://ol.d', 'http://ol.d']}, }]), + # fetch updated DID doc + requests_response({ + **DID_DOC, + 'alsoKnownAs': ['at://ne.w'], + }), ]) @patch('requests.post', return_value=requests_response('OK')) # update DID on PLC def test_set_username(self, mock_post, mock_get, mock_create_task, _): @@ -1246,6 +1251,8 @@ def test_set_username(self, mock_post, mock_get, mock_create_task, _): 'did': 'did:plc:user', }, timeout=15, stream=True, headers=ANY) + self.assertEqual('ne.w', user.handle_as(ATProto)) + # check #identity event seq = self.storage.last_seq(SUBSCRIBE_REPOS_NSID) self.assertEqual({ diff --git a/tests/test_models.py b/tests/test_models.py index 2fa108a5..238ecae5 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -282,6 +282,15 @@ def test_handle_as_web_custom_username(self, *_): self.assertEqual('alice', self.user.username()) self.assertEqual('@y.za@web.brid.gy', self.user.handle_as('ap')) + def test_handle_as_atproto_custom_handle(self, *_): + self.assertEqual('y.za.web.brid.gy', self.user.handle_as(ATProto)) + + self.user.copies = [Target(uri='did:plc:user', protocol='atproto')] + self.assertEqual('y.za.web.brid.gy', self.user.handle_as(ATProto)) + + self.store_object(id='did:plc:user', raw=DID_DOC) + self.assertEqual('ha.nl', self.user.handle_as(ATProto)) + def test_handle_as_None(self): class NoHandle(Fake): ABBREV = 'nohandle'